diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3bc45a9..cc1c632 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ jobs: strategy: matrix: os: [windows-latest, ubuntu-latest, macos-latest] - node-version: [10.x, 12.x, 14.x] + node-version: [10.x, 12.x, 14.x, 16.x] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..7c5cd16 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,19 @@ +name: Publish +on: + release: + types: [created] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + # Setup .npmrc file to publish to npm + - uses: actions/setup-node@v2 + with: + node-version: '12.x' + registry-url: 'https://registry.npmjs.org' + - run: npm install + - run: npm test + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file diff --git a/README.md b/README.md index 1fbe161..9df4033 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ ![Size](https://img.shields.io/bundlephobia/minzip/pactum) ![Platform](https://img.shields.io/node/v/pactum) -![Stars](https://img.shields.io/github/stars/pactumjs/pactum?style=social) -![Twitter](https://img.shields.io/twitter/follow/pactumjs?label=Follow&style=social) +[![Stars](https://img.shields.io/github/stars/pactumjs/pactum?style=social)](https://github.com/pactumjs/pactum/stargazers) +[![Twitter](https://img.shields.io/twitter/follow/pactumjs?label=Follow&style=social)](https://twitter.com/pactumjs) #### REST API Testing Tool for all levels in a Test Pyramid @@ -125,7 +125,7 @@ Scenario: Check Tea Pot ```js // steps.js const pactum = require('pactum'); -const { Given, When, Then, Before } = require('cucumber'); +const { Given, When, Then, Before } = require('@cucumber/cucumber'); let spec = pactum.spec(); @@ -180,7 +180,7 @@ Inspired from [frisby](https://docs.frisbyjs.com/) and [pact](https://docs.pact. ## Support -Like this project! Star it on [Github](https://github.com/pactumjs/pactum/stargazers). Your support means a lot to us. +Like this project! Star it on [Github](https://github.com/pactumjs/pactum/stargazers) and follow on [Twitter](https://twitter.com/pactumjs). Your support means a lot to us. ## Contributors diff --git a/package-lock.json b/package-lock.json index afdda90..b702685 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "pactum", - "version": "3.0.18", + "version": "3.0.19", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -299,9 +299,9 @@ } }, "@exodus/schemasafe": { - "version": "1.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.0.0-rc.3.tgz", - "integrity": "sha512-GoXw0U2Qaa33m3eUcxuHnHpNvHjNlLo0gtV091XBpaRINaB4X6FGCG5XKxSFNFiPpugUDqNruHzaqpTdDm4AOg==" + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.0.0-rc.4.tgz", + "integrity": "sha512-zHISeJ5jcHSo3i2bI5RHb0XEJ1JGxQ/QQzU2FLPcJxohNohJV8jHCM1FSrOUxTspyDRSSULg3iKQa1FJ4EsSiQ==" }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", @@ -580,9 +580,9 @@ "dev": true }, "centra": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/centra/-/centra-2.4.2.tgz", - "integrity": "sha512-f1RaP0V1HqVNEXfLfjNBthB2yy3KnSGnPCnOPCFLUk9e/Z4rNJ8nBaJNnghflnp88mi1IT8mfmW+HlMS1/H+bg==" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/centra/-/centra-2.5.0.tgz", + "integrity": "sha512-CnSF1HD8vOOgNbE4P2fZEhdhfAohvpcF3DSdSvEcSHDAZvr+Xfw73isT8SXJJc3VMBqSwjXhr29/ikHUgFcypg==" }, "chai": { "version": "4.3.4", @@ -1701,9 +1701,9 @@ "dev": true }, "phin": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/phin/-/phin-3.5.1.tgz", - "integrity": "sha512-jgFO28IaiWAl0xk+zmqVx7neKVokWKU8YTQC5QlB45SZnEE53LH2saqJIcyIV557VX3Gk+TdR4rwWTc3P83DSA==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/phin/-/phin-3.6.0.tgz", + "integrity": "sha512-nYY4Qh/yGCoxcwOAS2UfCM8+nVJcbI4f9NC4M4zPAsuswnIIS2aB14uYAbvdxP/4DqzXfrpadT2WQOx9mLDyTA==", "requires": { "centra": "^2.4.2" } diff --git a/package.json b/package.json index 1df976c..7e0fa70 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pactum", - "version": "3.0.18", + "version": "3.0.19", "description": "REST API Testing Tool for all levels in a Test Pyramid", "main": "./src/index.js", "types": "./src/index.d.ts", @@ -59,7 +59,7 @@ "author": "Anudeep ", "license": "MIT", "dependencies": { - "@exodus/schemasafe": "^1.0.0-rc.3", + "@exodus/schemasafe": "^1.0.0-rc.4", "deep-override": "^1.0.2", "form-data": "^4.0.0", "json-query": "^2.2.2", @@ -68,7 +68,7 @@ "openapi-fuzzer-core": "^1.0.6", "pactum-matchers": "^1.0.2", "parse-graphql": "^1.0.0", - "phin": "^3.5.1", + "phin": "^3.6.0", "polka": "^0.5.2" }, "devDependencies": { diff --git a/src/exports/expect.d.ts b/src/exports/expect.d.ts index 83b22e6..09671f1 100644 --- a/src/exports/expect.d.ts +++ b/src/exports/expect.d.ts @@ -18,6 +18,8 @@ export interface Have { jsonMatch(path: string, value: object): void; jsonMatchStrict(value: object): void; jsonMatchStrict(path: string, value: object): void; + jsonLength(value: number): void; + jsonLength(path: string, value: number): void; responseTimeLessThan(ms: number): void; error(err?: string | object): void; _(handler: string, data: any): Promise; diff --git a/src/exports/expect.js b/src/exports/expect.js index b7ad79a..e82251e 100644 --- a/src/exports/expect.js +++ b/src/exports/expect.js @@ -81,6 +81,11 @@ class Have { this._validate(); } + jsonLength(path, value) { + typeof value === 'undefined' ? this.expect.jsonLike.push(path) : this.expect.jsonLengthQuery.push({ path, value }); + this._validate(); + } + responseTimeLessThan(ms) { this.expect.responseTime = ms; this._validate(); diff --git a/src/models/Spec.d.ts b/src/models/Spec.d.ts index 64aa5da..e4e2fa2 100644 --- a/src/models/Spec.d.ts +++ b/src/models/Spec.d.ts @@ -316,6 +316,12 @@ declare class Spec { expectJsonMatchStrict(value: object): Spec; expectJsonMatchStrict(path: string, value: object): Spec; + /** + * expects the json to an array with length + */ + expectJsonLength(value: number): Spec; + expectJsonLength(path: string, value: number): Spec; + /** * expect network errors * @see https://pactumjs.github.io/#/response-validation?id=expecterror diff --git a/src/models/Spec.js b/src/models/Spec.js index 4082900..c95edd7 100644 --- a/src/models/Spec.js +++ b/src/models/Spec.js @@ -404,6 +404,11 @@ class Spec { return this; } + expectJsonLength(path, value) { + typeof value === 'undefined' ? this._expect.jsonLength.push(path) : this._expect.jsonLengthQuery.push({ path, value }); + return this; + } + expectError(error) { this._expect.errors.push(error); return this; diff --git a/src/models/expect.js b/src/models/expect.js index e5a64a1..d8d557e 100644 --- a/src/models/expect.js +++ b/src/models/expect.js @@ -32,6 +32,8 @@ class Expect { this.jsonMatchStrict = []; this.jsonMatchStrictQuery = []; this.jsonSnapshot = []; + this.jsonLength = []; + this.jsonLengthQuery = []; this.headers = []; this.headerContains = []; this.responseTime = null; @@ -57,6 +59,8 @@ class Expect { this._validateJsonMatchQuery(response); this._validateJsonMatchStrict(response); this._validateJsonMatchStrictQuery(response); + this._validateJsonLength(response); + this._validateJsonLengthQuery(response); this._validateJsonSnapshot(response); this._validateResponseTime(response); this._validateErrors(response); @@ -354,6 +358,33 @@ class Expect { } } + _validateJsonLength(response) { + this.jsonLength = processor.processData(this.jsonLength); + for (let i = 0; i < this.jsonLength.length; i++) { + const expected = this.jsonLength[i]; + if (response.json && Array.isArray(response.json)) { + const actual = response.json.length; + assert.strictEqual(actual, expected, `JSON Length ${actual} !== ${expected}`); + } else { + this.fail('Response does not contain a json array'); + } + } + } + + _validateJsonLengthQuery(response) { + this.jsonLengthQuery = processor.processData(this.jsonLengthQuery); + for (let i = 0; i < this.jsonLengthQuery.length; i++) { + const { path, value: expected } = this.jsonLengthQuery[i]; + const actualValue = jqy(path, { data: response.json }).value; + if (actualValue && Array.isArray(actualValue)) { + const actual = actualValue.length; + assert.strictEqual(actual, expected, `JSON Length ${actual} !== ${expected}`); + } else { + this.fail(`Response does not contain a json array at '${path}'`); + } + } + } + _validateResponseTime(response) { this.responseTime = processor.processData(this.responseTime); if (this.responseTime !== null) { diff --git a/src/models/server.js b/src/models/server.js index 36631a6..bb40d34 100644 --- a/src/models/server.js +++ b/src/models/server.js @@ -27,6 +27,7 @@ class Server { registerAllRoutes(this, this.app); this.app.listen(config.mock.port, () => { log.info(`Mock server is listening on port ${config.mock.port}`); + this._registerEvents(); resolve(); }); } else { @@ -78,6 +79,15 @@ class Server { } } + _registerEvents() { + process.on('SIGTERM', () => { + if (this.app) { + log.warn('Termination Signal Received - SIGTERM'); + this.stop(); + } + }); + } + } /** diff --git a/test/component/base.spec.js b/test/component/base.spec.js index 86661fe..bd67645 100644 --- a/test/component/base.spec.js +++ b/test/component/base.spec.js @@ -82,6 +82,22 @@ function addDefaultMockHandlers() { } }; }); + handler.addInteractionHandler('default users', () => { + return { + request: { + method: 'GET', + path: '/default/users' + }, + response: { + status: 200, + body: [ + { name: 'Matt', country: 'NZ' }, + { name: 'Pete', country: 'AU' }, + { name: 'Mike', country: 'NZ' } + ] + } + }; + }); } before(async () => { diff --git a/test/component/bdd.spec.js b/test/component/bdd.spec.js index 734c06c..098efc3 100644 --- a/test/component/bdd.spec.js +++ b/test/component/bdd.spec.js @@ -158,6 +158,26 @@ describe('BDD', () => { ce(spec.returns('name')).equals('snow'); }); + it('should not have a json with length', async () => { + let err; + try { + await expect(response).to.have.jsonLength(1); + } catch (error) { + err = error; + } + ce(err).not.undefined; + }); + + it('should not have a json with length at some path', async () => { + let err; + try { + await expect(response).to.have.jsonLength('path', 1); + } catch (error) { + err = error; + } + ce(err).not.undefined; + }); + }); describe('BDD - AutoReportRunner Disabled', () => { diff --git a/test/component/expects.spec.js b/test/component/expects.spec.js index 80d185e..2a66680 100644 --- a/test/component/expects.spec.js +++ b/test/component/expects.spec.js @@ -65,7 +65,8 @@ describe('Expects', () => { try { await pactum.spec() .get('http://localhost:9393/api/users/1') - .expect('isAddress'); + .expect('isAddress') + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -89,7 +90,8 @@ describe('Expects', () => { } }) .get('http://localhost:9393/api/address/1') - .expect('hasAddress', 'HOME'); + .expect('hasAddress', 'HOME') + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -114,7 +116,8 @@ describe('Expects', () => { }) .get('http://localhost:9393/api/users/1') .expect((res) => { expect(res.json).deep.equals({ id: 2 }); }) - .expectStatus(200); + .expectStatus(200) + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -126,7 +129,8 @@ describe('Expects', () => { try { await pactum.spec() .get('http://localhost:9393/api/users/1') - .expectStatus(200); + .expectStatus(200) + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -138,7 +142,8 @@ describe('Expects', () => { try { await pactum.spec() .get('http://localhost:9393/api/users/1') - .expectHeader('x-header', 'value'); + .expectHeader('x-header', 'value') + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -150,7 +155,8 @@ describe('Expects', () => { try { await pactum.spec() .get('http://localhost:9393/api/users/1') - .expectHeader('connection', 'value'); + .expectHeader('connection', 'value') + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -162,7 +168,8 @@ describe('Expects', () => { try { await pactum.spec() .get('http://localhost:9393/api/users/1') - .expectHeader('connection', /value/); + .expectHeader('connection', /value/) + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -174,7 +181,8 @@ describe('Expects', () => { try { await pactum.spec() .get('http://localhost:9393/api/users/1') - .expectHeaderContains('x-header', 'value'); + .expectHeaderContains('x-header', 'value') + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -186,7 +194,8 @@ describe('Expects', () => { try { await pactum.spec() .get('http://localhost:9393/api/users/1') - .expectHeaderContains('connection', 'value'); + .expectHeaderContains('connection', 'value') + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -198,7 +207,8 @@ describe('Expects', () => { try { await pactum.spec() .get('http://localhost:9393/api/users/1') - .expectHeaderContains('connection', /value/); + .expectHeaderContains('connection', /value/) + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -210,7 +220,8 @@ describe('Expects', () => { try { await pactum.spec() .get('http://localhost:9393/api/users/1') - .expectBody('Hello World'); + .expectBody('Hello World') + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -222,7 +233,8 @@ describe('Expects', () => { try { await pactum.spec() .get('http://localhost:9393/api/users/1') - .expectBodyContains('Hello World'); + .expectBodyContains('Hello World') + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -234,7 +246,8 @@ describe('Expects', () => { try { await pactum.spec() .get('http://localhost:9393/api/users/1') - .expectBodyContains({ msg: 'Hello World' }); + .expectBodyContains({ msg: 'Hello World' }) + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -246,7 +259,8 @@ describe('Expects', () => { try { await pactum.spec() .get('http://localhost:9393/api/users/1') - .expectBodyContains(/Hello World/); + .expectBodyContains(/Hello World/) + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -258,7 +272,8 @@ describe('Expects', () => { try { await pactum.spec() .get('http://localhost:9393/api/users/1') - .expectJsonLike({ id: 1 }); + .expectJsonLike({ id: 1 }) + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -284,7 +299,8 @@ describe('Expects', () => { .get('http://localhost:9393/api/users/1') .expectJsonSchema({ "required": ["userId", "id"] - }); + }) + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -310,7 +326,8 @@ describe('Expects', () => { .get('http://localhost:9393/api/users/1') .expectJsonSchema('id', { "type": "string" - }); + }) + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -322,7 +339,8 @@ describe('Expects', () => { try { await pactum.spec() .get('http://localhost:9393/api/users/1') - .expectJsonMatch(like({ id: 1 })); + .expectJsonMatch(like({ id: 1 })) + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -336,7 +354,8 @@ describe('Expects', () => { .get('http://localhost:9394/api/users/1') .expectJsonSchema({ "required": ["userId", "id"] - }); + }) + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -357,7 +376,8 @@ describe('Expects', () => { } }) .get('http://localhost:9393/api/users/1') - .expectStatus(200); + .expectStatus(200) + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -384,7 +404,8 @@ describe('Expects', () => { } }) .get('http://localhost:9393/api/users/1') - .expectStatus(200); + .expectStatus(200) + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -408,7 +429,8 @@ describe('Expects', () => { } }) .get('http://localhost:9393/api') - .expectStatus(200); + .expectStatus(200) + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -432,7 +454,8 @@ describe('Expects', () => { } }) .get('http://localhost:9393/api') - .expectStatus(200); + .expectStatus(200) + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -509,7 +532,8 @@ describe('Expects', () => { .get('http://localhost:9393/api/users') .expectStatus(200) .expectJson('people[country=NZ].name', 'Matt') - .expectJson('people[*].name', ['Matt', 'Pete']); + .expectJson('people[*].name', ['Matt', 'Pete']) + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -561,7 +585,8 @@ describe('Expects', () => { }) .get('http://localhost:9393/api/users') .expectStatus(200) - .expectJsonLike('people[*].name', ['Matt', 'Pet']); + .expectJsonLike('people[*].name', ['Matt', 'Pet']) + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -573,7 +598,8 @@ describe('Expects', () => { try { await pactum.spec() .get('http://localhost:9393/api/users/1') - .expectResponseTime(-1); + .expectResponseTime(-1) + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -625,7 +651,8 @@ describe('Expects', () => { }) .get('http://localhost:9393/api/users') .expectStatus(200) - .expectJsonMatch('people[*].name', eachLike(12)); + .expectJsonMatch('people[*].name', eachLike(12)) + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -647,7 +674,8 @@ describe('Expects', () => { .useInteraction('get people') .get('http://localhost:9393/api/people') .expectStatus(200) - .expectJsonMatchStrict('people[*].name', eachLike(12)); + .expectJsonMatchStrict('people[*].name', eachLike(12)) + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -675,7 +703,8 @@ describe('Expects', () => { .useInteraction('get people') .get('http://localhost:9393/api/people') .expectStatus(200) - .expectJsonMatchStrict({}); + .expectJsonMatchStrict({}) + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -736,7 +765,8 @@ describe('Expects', () => { .name('json snapshot - deep equal') .get('http://localhost:9393/api/users/1') .expectStatus(200) - .expectJsonSnapshot(); + .expectJsonSnapshot() + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -859,7 +889,8 @@ describe('Expects', () => { .expectStatus(200) .expectJsonSnapshot({ id: like('id') - }); + }) + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -897,7 +928,8 @@ describe('Expects', () => { .expectStatus(200) .expectJsonSnapshot({ id: like('id') - }); + }) + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -935,7 +967,8 @@ describe('Expects', () => { .expectStatus(200) .expectJsonSnapshot({ id: like('id') - }); + }) + .useLogLevel('ERROR'); } catch (error) { err1 = error; } @@ -947,7 +980,8 @@ describe('Expects', () => { .expectStatus(200) .expectJsonSnapshot({ id: like(1) - }); + }) + .useLogLevel('ERROR'); } catch (error) { err2 = error; } @@ -997,7 +1031,8 @@ describe('Expects', () => { await pactum.spec() .useInteraction('default get') .get('http://localhost:9393/default/get') - .expectError(); + .expectError() + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -1009,7 +1044,8 @@ describe('Expects', () => { try { await pactum.spec() .get('http://localhost:9392') - .expectError('ECONNRESET'); + .expectError('ECONNRESET') + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -1021,7 +1057,8 @@ describe('Expects', () => { try { await pactum.spec() .get('http://localhost:9392') - .expectError({ code: 'ECONNRESET' }); + .expectError({ code: 'ECONNRESET' }) + .useLogLevel('ERROR'); } catch (error) { err = error; } @@ -1061,4 +1098,74 @@ describe('Expects', () => { }); }); + it('json length', async () => { + await pactum.spec() + .useInteraction('default users') + .get('http://localhost:9393/default/users') + .expectJsonLength(3); + }); + + it('json length - fail', async () => { + let err; + try { + await pactum.spec() + .useInteraction('default users') + .get('http://localhost:9393/default/users') + .expectJsonLength(2) + .useLogLevel('ERROR'); + } catch (error) { + err = error; + } + expect(err).not.undefined; + }); + + it('json length - fail - invalid', async () => { + let err; + try { + await pactum.spec() + .useInteraction('default get') + .get('http://localhost:9393/default/get') + .expectJsonLength(2) + .useLogLevel('ERROR'); + } catch (error) { + err = error; + } + expect(err).not.undefined; + }); + + it('json length query', async () => { + await pactum.spec() + .useInteraction('get people') + .get('http://localhost:9393/api/people') + .expectJsonLength('people', 3); + }); + + it('json length - fail', async () => { + let err; + try { + await pactum.spec() + .useInteraction('get people') + .get('http://localhost:9393/api/people') + .expectJsonLength('people', 2) + .useLogLevel('ERROR'); + } catch (error) { + err = error; + } + expect(err).not.undefined; + }); + + it('json length - fail - invalid', async () => { + let err; + try { + await pactum.spec() + .useInteraction('get people') + .get('http://localhost:9393/api/people') + .expectJsonLength('people[0]', 2) + .useLogLevel('ERROR'); + } catch (error) { + err = error; + } + expect(err).not.undefined; + }); + }); \ No newline at end of file