Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: integration testing for the MQTT protocol #228

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
9d0c575
feat: refactoring handlers to separate from generated code
kaushik-rishi Aug 15, 2023
9655061
export the app from template/index.js file
kaushik-rishi Aug 23, 2023
e47c79b
add documentation about adding custom code
kaushik-rishi Aug 24, 2023
91ac693
update jsdoc
kaushik-rishi Aug 25, 2023
eb89354
change exports.app to module.exports
kaushik-rishi Aug 25, 2023
df8d2ed
Merge branch 'master' into master
kaushik-rishi Aug 30, 2023
819c330
Merge branch 'master' into master
derberg Aug 30, 2023
4b1ea37
Merge branch 'asyncapi:master' into master
kaushik-rishi Sep 12, 2023
c0c75d6
feat: enable template to be installed an used as a library
kaushik-rishi Sep 21, 2023
dcaf95d
Merge branch 'master' of github.com:kaushik-rishi/nodejs-template
kaushik-rishi Sep 21, 2023
8cf1367
docs: allowing the template to be imported as a library
kaushik-rishi Sep 21, 2023
35c42de
tests: update snapshot tests
kaushik-rishi Sep 22, 2023
d9ed24b
use register<OperationId>Middleware method to register middlewares
kaushik-rishi Oct 1, 2023
7427f29
docs: usage of register<OperationId>Middleware
kaushik-rishi Oct 1, 2023
df2310b
docs: usage of register<OperationId>Middleware
kaushik-rishi Oct 1, 2023
a0c8f75
tests: update snapshot tests
kaushik-rishi Oct 1, 2023
36da813
add test server configuration to mqtt mock spec file
kaushik-rishi Oct 2, 2023
159e04e
tests: add integration tests for mqtt protocol
kaushik-rishi Oct 2, 2023
0dea140
Merge branch 'master' into integration-testing
kaushik-rishi Oct 2, 2023
5afd14e
simplify integration test folder structure
kaushik-rishi Oct 2, 2023
7a7bb8c
Revert "simplify integration test folder structure"
kaushik-rishi Oct 4, 2023
def0198
simplify integration test folder structure single layer
kaushik-rishi Oct 4, 2023
93a4670
add forceExit jest
kaushik-rishi Oct 4, 2023
ca95edb
add generation script for integration test examples
kaushik-rishi Oct 4, 2023
3b53332
setup test action
kaushik-rishi Oct 4, 2023
4439dd2
move action into workflows
kaushik-rishi Oct 4, 2023
1691ec7
move action into workflows
kaushik-rishi Oct 4, 2023
b786144
Remove package.json checks from workflow
kaushik-rishi Oct 4, 2023
8e1b521
update workflow
kaushik-rishi Oct 4, 2023
264cf57
update workflow
kaushik-rishi Oct 4, 2023
dd951fb
test with production instance
kaushik-rishi Oct 4, 2023
2ec3dfb
Revert "test with production instance"
kaushik-rishi Oct 4, 2023
de5df6d
add port sharing docker compose mosquitto
kaushik-rishi Oct 4, 2023
552655f
pushing error
kaushik-rishi Oct 4, 2023
e25dea0
Revert "pushing error"
kaushik-rishi Oct 4, 2023
168fb1a
disabling snapshot tests
kaushik-rishi Oct 4, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/testing-integration-test-gh-action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Testing working of github actions

on:
push:
# Sequence of patterns matched against refs/heads
branches:
- 'integration-testing'

jobs:
test:
name: 'Run example tests'
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: 14
- name: Install dependencies
run: npm ci
- name: Build the docker-compose stack
run: cd test/integration-tests && docker-compose up -d
- name: Check running containers
run: docker ps
- name: Setup libraries and dependencies for integration testing examples
run: npm run setup:integrationTestExamples
- name: Test examples
run: npm run test
- name: Check running containers
run: docker ps
- name: Dump docker logs on failure
if: failure()
uses: jwalton/gh-docker-logs@v1
103 changes: 103 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- [Supported protocols](#supported-protocols)
- [How to use the template](#how-to-use-the-template)
* [CLI](#cli)
* [Adding custom code](#adding-custom-code--handlers)
- [Template configuration](#template-configuration)
- [Development](#development)
- [Contributors](#contributors)
Expand Down Expand Up @@ -100,6 +101,108 @@ $ mqtt pub -t 'smartylighting/streetlights/1/0/event/123/lighting/measured' -h '
#Notice that the server automatically validates incoming messages and logs out validation errors
```

### Adding custom code / handlers

It's highly recommended to treat the generated template as a library or API for initializing the server and integrating user-written handlers. Instead of directly modifying the template, leveraging it in this manner ensures that its regenerative capability is preserved. Any modifications made directly to the template would be overwritten upon regeneration.

Consider a scenario where you intend to introduce a new channel or section to the AsyncAPI file, followed by a template regeneration. In this case, any modifications applied within the generated code would be overwritten.

To avoid this, user code remains external to the generated code, functioning as an independent entity that consumes the generated code as a library. By adopting this approach, the user code remains unaffected during template regenerations.

Facilitating this separation involves creating handlers and associating them with their respective routes. These handlers can then be seamlessly integrated into the template's workflow by importing the appropriate methods to register the handlers. In doing so, the template's `client.register<operationId>Middleware` method becomes the bridge between the user-written handlers and the generated code. This can be used to register middlewares for specific methods on specific channels.

> The AsyncAPI file used for the example is [here](https://bit.ly/asyncapi)

```js
// output refers to the generated template folder
// You require the generated server. Running this code starts the server
// App exposes API to send messages
const { client } = require("./output");

// to start the app
client.init();

// Generated handlers that we use to react on consumer / produced messages are attached to the client
// through which we can register middleware functions

/**
*
*
* Example of how to process a message before it is sent to the broker
*
*
*/
function testPublish() {
// mosquitto_sub -h test.mosquitto.org -p 1883 -t "smartylighting/streetlights/1/0/action/12/turn/on"

// Registering your custom logic in a channel-specific handler
// the passed handler function is called once the app sends a message to the channel
// For example `client.app.send` sends a message to some channel using and before it is sent, you want to perform some other actions
// in such a case, you can register middlewares like below
client.registerTurnOnMiddleware((message) => { // `turnOn` is the respective operationId
console.log("hitting the middleware before publishing the message");
console.log(
`sending turn on message to streetlight ${message.params.streetlightId}`,
message.payload
);
});

client.app.send(
{ command: "off" },
{},
"smartylighting/streetlights/1/0/action/12/turn/on"
);
}


/**
*
*
* Example of how to work with generated code as a consumer
*
*
*/
function testSubscribe() {
// mosquitto_pub -h test.mosquitto.org -p 1883 -t "smartylighting/streetlights/1/0/event/101/lighting/measured" -m '{"lumens": 10}'

// Writing your custom logic that should be triggered when your app receives as message from a given channel
// Registering your custom logic in a channel-specific handler
// the passed handler functions are called once the app gets message sent to the channel

client.registerReceiveLightMeasurementMiddleware((message) => { // `recieveLightMeasurement` is the respective operationId
console.log("recieved in middleware 1", message.payload);
});

client.registerReceiveLightMeasurementMiddleware((message) => {
console.log("recieved in middleware 2", message.payload);
});
}

testPublish();
testSubscribe();

/**
*
*
* Example of how to produce a message using API of generated app independently from the handlers
*
*
*/

(function myLoop (i) {
setTimeout(() => {
console.log('producing custom message');
client.app.send({percentage: 1}, {}, 'smartylighting/streetlights/1/0/action/1/turn/on');
if (--i) myLoop(i);
}, 1000);
}(3));
```

You can run the above code and test the working of the handlers by sending a message using the mqtt cli / mosquitto broker software to the `smartylighting/streetlights/1/0/event/123/lighting/measured` channel using this command
`mosquitto_pub -h test.mosquitto.org -p 1883 -t "smartylighting/streetlights/1/0/event/101/lighting/measured" -m '{"lumens": 10, "sentAt": "2017-06-07T12:34:32.000Z"}'`
or
`mqtt pub -t 'smartylighting/streetlights/1/0/event/123/lighting/measured' -h 'test.mosquitto.org' -m '{"id": 1, "lumens": 3, }'` (if you are using the mqtt cli)

## Template configuration

You can configure this template by passing different parameters in the Generator CLI: `-p PARAM1_NAME=PARAM1_VALUE -p PARAM2_NAME=PARAM2_VALUE`
Expand Down
6 changes: 6 additions & 0 deletions filters/all.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ function trimLastChar(string) {
}
filter.trimLastChar = trimLastChar;

function convertOpertionIdToMiddlewareFn(operationId) {
const capitalizedOperationId = operationId.charAt(0).toUpperCase() + operationId.slice(1);
return "register" + capitalizedOperationId + "Middleware";
}
filter.convertOpertionIdToMiddlewareFn = convertOpertionIdToMiddlewareFn;

function toJS(objFromJSON, indent = 2) {
if (typeof objFromJSON !== 'object' || Array.isArray(objFromJSON)) {
// not an object, stringify using native function
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@
},
"homepage": "https://github.com/asyncapi/nodejs-template#readme",
"scripts": {
"test": "rimraf test/temp && jest --modulePathIgnorePatterns='./template'",
"test": "rimraf test/temp && jest --forceExit --modulePathIgnorePatterns='./template'",
"test:updateSnapshot": "rimraf test/temp && jest --updateSnapshot --modulePathIgnorePatterns='./template'",
"lint": "eslint --max-warnings 0 --config .eslintrc .",
"lint:fix": "eslint --fix --config .eslintrc .",
"generate:assets": "npm run generate:readme:toc",
"generate:readme:toc": "markdown-toc -i README.md",
"bump:version": "npm --no-git-tag-version --allow-same-version version $VERSION"
"bump:version": "npm --no-git-tag-version --allow-same-version version $VERSION",
"setup:integrationTestExamples": "cd scripts && node setup-integration-tests.js"
},
"publishConfig": {
"access": "public"
Expand Down
37 changes: 37 additions & 0 deletions scripts/setup-integration-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const fs = require('fs');
const path = require('path');
const examplePath = path.resolve(__dirname, '..', 'test', 'integration-tests');
const util = require('util');
const exec = util.promisify(require('child_process').exec);

const { exit } = require('process');

const promises = fs.readdirSync(examplePath)
.map((file) => { return path.resolve(examplePath, file); })
.filter((exampleDir) => { return fs.lstatSync(exampleDir).isDirectory(); })
.map((exampleDir) => {
let command = 'generate:client';
const generatedLibraryPath = path.resolve(exampleDir, 'nodejs-client');
if (fs.existsSync(generatedLibraryPath)) {
fs.rmSync(generatedLibraryPath, {
recursive: true,
force: true
});
}
return exec(`cd ${exampleDir} && npm run setup`);
});

/*
async function main() {
for (let promise of promises) {
await promise;
}
}

main();
*/

Promise.all(promises).catch((err) => {
console.error(err);
exit(1);
});
1 change: 1 addition & 0 deletions template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "{{ asyncapi.info().title() | kebabCase }}",
"description": "{{ asyncapi.info().description() | oneLine }}",
"version": "{{ asyncapi.info().version() }}",
"main": "./src/api",
"scripts": {
"start": "node src/api/index.js"
},
Expand Down
70 changes: 56 additions & 14 deletions template/src/api/handlers/$$channel$$.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
{%- if channel.hasPublish() and channel.publish().ext('x-lambda') %}const fetch = require('node-fetch');{%- endif %}
const handler = module.exports = {};

{% if channel.hasPublish() %}
const {{ channel.publish().id() }}Middlewares = [];

/**
* Registers a middleware function for the {{ channel.publish().id() }} operation to be executed during request processing.
*
* Middleware functions have access to options object that you can use to access the message content and other helper functions
*
* @param {function} middlewareFn - The middleware function to be registered.
* @throws {TypeError} If middlewareFn is not a function.
*/
handler.{{ channel.publish().id() | convertOpertionIdToMiddlewareFn }} = (middlewareFn) => {
if (typeof middlewareFn !== 'function') {
throw new TypeError('middlewareFn must be a function');
}
{{ channel.publish().id() }}Middlewares.push(middlewareFn);
}

/**
* {{ channel.publish().summary() }}
*
* @param {object} options
* @param {object} options.message
{%- if channel.publish().message(0).headers() %}
Expand All @@ -16,7 +35,7 @@ const handler = module.exports = {};
{%- endfor %}
{%- endif %}
*/
handler.{{ channel.publish().id() }} = async ({message}) => {
handler._{{ channel.publish().id() }} = async ({message}) => {
{%- if channel.publish().ext('x-lambda') %}
{%- set lambda = channel.publish().ext('x-lambda') %}
fetch('{{ lambda.url }}', {
Expand All @@ -30,29 +49,52 @@ handler.{{ channel.publish().id() }} = async ({message}) => {
.then(json => console.log(json))
.catch(err => { throw err; });
{%- else %}
// Implement your business logic here...
for (const middleware of {{ channel.publish().id() }}Middlewares) {
await middleware(message);
}
{%- endif %}
};

{%- endif %}

{%- if channel.hasSubscribe() %}
const {{ channel.subscribe().id() }}Middlewares = [];

/**
* Registers a middleware function for the {{ channel.subscribe().id() }} operation to be executed during request processing.
*
* Middleware functions have access to options object that you can use to access the message content and other helper functions
*
* @param {function} middlewareFn - The middleware function to be registered.
* @throws {TypeError} If middlewareFn is not a function.
*/
handler.{{ channel.subscribe().id() | convertOpertionIdToMiddlewareFn }} = (middlewareFn) => {
if (typeof middlewareFn !== 'function') {
throw new TypeError('middlewareFn must be a function');
}
{{ channel.subscribe().id() }}Middlewares.push(middlewareFn);
}

/**
* {{ channel.subscribe().summary() }}
*
* @param {object} options
* @param {object} options.message
{%- if channel.subscribe().message(0).headers() %}
{%- for fieldName, field in channel.subscribe().message(0).headers().properties() %}
{{ field | docline(fieldName, 'options.message.headers') }}
{%- endfor %}
{%- endif %}
{%- if channel.subscribe().message(0).payload() %}
{%- for fieldName, field in channel.subscribe().message(0).payload().properties() %}
{{ field | docline(fieldName, 'options.message.payload') }}
{%- endfor %}
{%- endif %}
{%- if channel.subscribe().message(0).headers() %}
{%- for fieldName, field in channel.subscribe().message(0).headers().properties() %}
{{ field | docline(fieldName, 'options.message.headers') }}
{%- endfor %}
{%- endif %}
{%- if channel.subscribe().message(0).payload() %}
{%- for fieldName, field in channel.subscribe().message(0).payload().properties() %}
{{ field | docline(fieldName, 'options.message.payload') }}
{%- endfor %}
{%- endif %}
*/
handler.{{ channel.subscribe().id() }} = async ({message}) => {
// Implement your business logic here...
handler._{{ channel.subscribe().id() }} = async ({message}) => {
for (const middleware of {{ channel.subscribe().id() }}Middlewares) {
await middleware(message);
}
};

{%- endif %}
41 changes: 32 additions & 9 deletions template/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,35 @@ app.useOutbound(errorLogger);
app.useOutbound(logger);
app.useOutbound(json2string);

app
.listen()
.then((adapters) => {
console.log(cyan.underline(`${config.app.name} ${config.app.version}`), gray('is ready!'), '\n');
adapters.forEach(adapter => {
console.log('🔗 ', adapter.name(), gray('is connected!'));
});
})
.catch(console.error);
function init() {
app
.listen()
.then((adapters) => {
console.log(cyan.underline(`${config.app.name} ${config.app.version}`), gray('is ready!'), '\n');
adapters.forEach(adapter => {
console.log('🔗 ', adapter.name(), gray('is connected!'));
});
})
.catch(console.error);
}

const handlers = {
{%- for channelName, channel in asyncapi.channels() -%}
{% if channel.hasPublish() %}
{{ channel.publish().id() | convertOpertionIdToMiddlewareFn }}: require('./handlers/{{ channelName | convertToFilename }}').{{ channel.publish().id() | convertOpertionIdToMiddlewareFn }},
{%- endif -%}
{% if channel.hasSubscribe() %}
{{ channel.subscribe().id() | convertOpertionIdToMiddlewareFn }}: require('./handlers/{{ channelName | convertToFilename }}').{{ channel.subscribe().id() | convertOpertionIdToMiddlewareFn }},
{% endif %}
{%- endfor -%}
};

const client = {
app,
init,
...handlers
};

module.exports = {
client
};
Loading