From b27aee66a7ee52b1fdd0961bcbf1cefdfe9fd68f Mon Sep 17 00:00:00 2001 From: Nora Date: Sun, 26 Apr 2020 01:12:59 -0400 Subject: [PATCH] feat(example-lb3-application): add lb3 tests to mocha --- examples/lb3-application/README.md | 196 +++++++++++++++++- .../lb3app/common/models/coffee-shop.js | 2 +- .../lb3-application/lb3app/test/acceptance.js | 107 ++++++++++ .../lb3app/test/authentication.js | 135 ++++++++++++ .../lb3app/test/integration.js | 75 +++++++ examples/lb3-application/package-lock.json | 54 +++++ examples/lb3-application/package.json | 5 +- .../__tests__/acceptance/lb3app.acceptance.ts | 2 +- examples/lb3-application/src/server.ts | 2 +- packages/booter-lb3app/README.md | 6 + 10 files changed, 577 insertions(+), 7 deletions(-) create mode 100644 examples/lb3-application/lb3app/test/acceptance.js create mode 100644 examples/lb3-application/lb3app/test/authentication.js create mode 100644 examples/lb3-application/lb3app/test/integration.js diff --git a/examples/lb3-application/README.md b/examples/lb3-application/README.md index 18d4b1ed54f7..07cbeadd3184 100644 --- a/examples/lb3-application/README.md +++ b/examples/lb3-application/README.md @@ -337,13 +337,205 @@ $ npm start Load [http://localhost:3000/](http://localhost:3000/) on your browser. This will load the Express app, with mounted LB3 and LB4 applications. -### Need help? +## Running LB3 tests from LB4 + +You can run tests in an LoopBack 3 application from the LoopBack 4 application +it mounted on with command `npm test`. + +We want the LoopBack 3 tests to use the LoopBack 4 server rather than the +LoopBack 3 application. The following guide shows how to run + +- acceptance-level tests making HTTP calls to invoke application logic. e.g. + `POST /users/login` +- integration-level tests that are using JS API to call application logic. e.g. + `MyModel.create()` + +### Adding LB3 Test Path in Command + +In order to run LoopBack 3's tests from their current folder, add LB3 tests' +path to `test` entry in package.json: + +- `"test": "lb-mocha \"dist/**tests**/\*_/_.js\" \"lb3app/test/\*.js\""` + +In this case, the test folder is +[`/lb3app/test`](https://github.com/strongloop/loopback-next/tree/spike/lb3test/examples/lb3-application/lb3app/test) +from the root of the LoopBack 4 project. + +This will run LoopBack 4 tests first then LoopBack 3 tests. + +_To emphasize the setup steps and separate them from the test case details, all +the comprehensive test code are extracted into function `runTests`._ + +### Running Acceptance Tests + +First, move any LoopBack 3 test dependencies to `package.json`'s devDependencies +and run: + +```sh +npm install +``` + +In your test file: + +1. When launch the Express server + +- 1.1 Update to use the Express server when doing requests: + + ```ts + // can use lb4's testlab's supertest as the dependency is already installed + const {supertest} = require('@loopback/testlab'); + const assert = require('assert'); + const should = require('should'); + const {ExpressServer} = require('../../dist/server'); + + let app; + + function jsonForExpressApp(verb, url) { + // use the express server, it mounts LoopBack 3 apis to + // base path '/api' + return supertest(app.server) + [verb]('/api' + url) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .expect('Content-Type', /json/); + } + ``` + +- 1.2 Boot and start the Express app in your before hook, and stop the app in + the after hook: + + ```ts + describe('LoopBack 3 style tests - Launch Express server', function () { + before(async function () { + app = new ExpressServer(); + await app.boot(); + await app.start(); + }); + + after(async () => { + await app.stop(); + }); + + // your tests here + runTests(); + }); + ``` + +2. When launch the LoopBack 4 application + +- 2.1 Update to use the LoopBack 4 server when doing requests: + + ```ts + // can use lb4's testlab's supertest as the dependency is already installed + const {supertest} = require('@loopback/testlab'); + const assert = require('assert'); + const should = require('should'); + const {CoffeeShopApplication} = require('../../dist/application'); + + let app; + + function jsonForLB4(verb, url) { + // use the lb4 app's rest server + return supertest(app.restServer.url) + [verb](url) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .expect('Content-Type', /json/); + } + ``` + +- 2.2 Boot and start the LoopBack 4 app in your before hook, and stop the app in + the after hook: + + ```ts + describe('LoopBack 3 style tests - launch LB4 app', function () { + before(async function () { + app = new CoffeeShopApplication(); + await app.boot(); + await app.start(); + }); + + after(async () => { + await app.stop(); + }); + + // your tests here + runTests(); + }); + ``` + +Example of this use can be seen in +[`test/acceptance.js`](https://github.com/strongloop/loopback-next/tree/master/examples/lb3-application/lb3app/test/acceptance.js) +which has the same tests as +[`src/__tests__/acceptance/lb3app.acceptance.ts`](https://github.com/strongloop/loopback-next/blob/spike/lb3test/examples/lb3-application/src/__tests__/acceptance/lb3app.acceptance.ts), +but in LB3 style. And +[`test/authentication.js`](https://github.com/strongloop/loopback-next/tree/master/examples/lb3-application/lb3app/test/authentication.js) + +Now when you run `npm test` your LoopBack 3 tests should be run along with any +LoopBack 4 tests you have. + +Optional: Another option is to migrate your tests to use LoopBack 4 style of +testing, similar to `src/__tests__/acceptance/lb3app.acceptance.ts`. +Documentation for LoopBack testing can be found in +https://loopback.io/doc/en/lb4/Testing-your-application.html. + +## Running Integration Tests + +For the integration tests, LoopBack 3 models were bound to the LoopBack 4 +application in order to allow JavaScript API to call application logic such as +`Model.create()`. This can be seen in +[`packages/booter-lb3app/src/lb3app.booter.ts`](https://github.com/strongloop/loopback-next/blob/spike/lb3test/packages/booter-lb3app/src/lb3app.booter.ts#L76-L85). + +In order to retrieve the model from the application's context, `get()` can be +used as follows: + +```ts +describe('LoopBack 3 style integration tests', function () { + let app; + let CoffeeShop; + + before(async function () { + // If launch the LoopBack 4 application + // app = new CoffeeShopApplication(); + app = new ExpressServer(); + await app.boot(); + await app.start(); + }); + + before(() => { + // follow the syntax: lb3-models.{ModelName} + // If launch the LoopBack 4 application + // CoffeeShop = await app.get('lb3-models.CoffeeShop'); + CoffeeShop = await app.lbApp.get('lb3-models.CoffeeShop'); + }); + + after(async () => { + await app.stop(); + }); + + // your tests here + runTests(); +}); +``` + +The syntax for LB3 model's binding key is `lb3-models.{model name}`. + +Additionally, LB3 datasources are also bound to the LB4 application's context +and can be retrieved with a key in the syntax `lb3-datasources.{ds name}`. + +Example integration tests can be found in +[`examples/lb3-application/lb3app/test/integration.js`](https://github.com/strongloop/loopback-next/tree/master/examples/lb3-application/lb3app/test/integration.js). + +Example authentication tests can be found in +[`examples/lb3-application/lb3app/test/authentication.js`](https://github.com/strongloop/loopback-next/tree/master/examples/lb3-application/lb3app/test/authentication.js). + +## Need help? Check out our [Slack](https://join.slack.com/t/loopbackio/shared_invite/zt-8lbow73r-SKAKz61Vdao~_rGf91pcsw) and ask for help with this tutorial. -### Bugs/Feedback +## Bugs/Feedback Open an issue in [loopback-next](https://github.com/strongloop/loopback-next) and we'll take a look. diff --git a/examples/lb3-application/lb3app/common/models/coffee-shop.js b/examples/lb3-application/lb3app/common/models/coffee-shop.js index 4917a2f582ef..c88f44497b2c 100755 --- a/examples/lb3-application/lb3app/common/models/coffee-shop.js +++ b/examples/lb3-application/lb3app/common/models/coffee-shop.js @@ -38,6 +38,6 @@ module.exports = function (CoffeeShop) { }; CoffeeShop.remoteMethod('greet', { http: {path: '/greet', verb: 'get'}, - returns: {type: 'string'}, + returns: {arg: 'greeting', type: 'string'}, }); }; diff --git a/examples/lb3-application/lb3app/test/acceptance.js b/examples/lb3-application/lb3app/test/acceptance.js new file mode 100644 index 000000000000..990919419d91 --- /dev/null +++ b/examples/lb3-application/lb3app/test/acceptance.js @@ -0,0 +1,107 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-lb3-application +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +const {supertest} = require('@loopback/testlab'); +const assert = require('assert'); +const {ExpressServer} = require('../../dist/server'); +const {CoffeeShopApplication} = require('../../dist/application'); +require('should'); + +let app; + +function jsonForLB4(verb, url) { + // use the lb4 app's rest server + return supertest(app.restServer.url) + [verb](url) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .expect('Content-Type', /json/); +} + +function jsonForExpressApp(verb, url) { + // use the express server, it mounts LoopBack 3 apis to + // base path '/api' + return supertest(app.server) + [verb]('/api' + url) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .expect('Content-Type', /json/); +} + +function jsonForExternal(verb, url) { + // use the express server, its external apis doesn't have base path + return supertest(app.server) + [verb](url) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .expect('Content-Type', /json/); +} + +describe('LoopBack 3 style acceptance tests - boot from Express server', function () { + before(async function () { + app = new ExpressServer(); + await app.boot(); + await app.start(); + }); + + after(async () => { + await app.stop(); + }); + + it('gets external route in application', function (done) { + jsonForExternal('get', '/ping').expect(200, function (err, res) { + assert.equal(res.text, 'pong'); + done(); + }); + }); + + runTests(jsonForExpressApp); +}); + +describe('LoopBack 3 style acceptance tests - boot from LB4 app', function () { + before(async function () { + app = new CoffeeShopApplication(); + await app.boot(); + await app.start(); + }); + + after(async () => { + await app.stop(); + }); + + runTests(jsonForLB4); +}); + +function runTests(request) { + context('basic REST calls for LoopBack 3 application', () => { + it('creates and finds a CoffeeShop', function (done) { + request('post', '/CoffeeShops') + .send({ + name: 'Coffee Shop', + city: 'Toronto', + }) + .expect(200) + .end(function (err, res) { + assert(typeof res.body === 'object'); + assert(res.body.name); + assert(res.body.city); + assert.equal(res.body.name, 'Coffee Shop'); + assert.equal(res.body.city, 'Toronto'); + done(); + }); + }); + + it("gets the CoffeeShop's status", function (done) { + request('get', '/CoffeeShops/status').expect(200, function (err, res) { + res.body.status.should.be.equalOneOf( + 'We are open for business.', + 'Sorry, we are closed. Open daily from 6am to 8pm.', + ); + done(); + }); + }); + }); +} diff --git a/examples/lb3-application/lb3app/test/authentication.js b/examples/lb3-application/lb3app/test/authentication.js new file mode 100644 index 000000000000..56913b952733 --- /dev/null +++ b/examples/lb3-application/lb3app/test/authentication.js @@ -0,0 +1,135 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-lb3-application +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +const lb3App = require('../server/server'); +const {supertest} = require('@loopback/testlab'); +const assert = require('assert'); +const {ExpressServer} = require('../../dist/server'); +const {CoffeeShopApplication} = require('../../dist/application'); +require('should'); + +let app, User; + +function jsonForLB4(verb, url) { + // use the lb4 app's rest server + return supertest(app.restServer.url) + [verb](url) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .expect('Content-Type', /json/); +} + +function jsonForExpressApp(verb, url) { + // use the express server, it mounts apis to base path '/api' + return supertest(app.server) + [verb]('/api' + url) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .expect('Content-Type', /json/); +} + +/** + * The tests show running LoopBack 3 authentication tests mounted to + * an Express server. + * Make sure you start the express server first and stop it after tests done. + */ +describe('LoopBack 3 authentication - Express server', function () { + before(async function () { + User = lb3App.models.User; + app = new ExpressServer(); + await app.boot(); + await app.start(); + }); + + after(async () => { + await User.destroyAll(); + await app.stop(); + }); + + runTests(jsonForExpressApp); +}); + +/** + * The tests show running LoopBack 3 authentication tests mounted to + * a LoopBack 4 application. + * Make sure you start the LoopBack 4 application first and stop it + * after tests done. + */ +describe('Loopback 3 authentication - LoopBack 4 app', function () { + before(async function () { + User = lb3App.models.User; + app = new CoffeeShopApplication(); + await app.boot(); + await app.start(); + }); + + after(async () => { + await User.destroyAll(); + await app.stop(); + }); + + runTests(jsonForLB4); +}); + +function runTests(request) { + it('creates a User and logs them in and out', function (done) { + // create user + request('post', '/users') + .send({email: 'new@email.com', password: 'L00pBack!'}) + .expect(200, function (err, user) { + assert.equal(user.body.email, 'new@email.com'); + // login + request('post', '/users/login') + .send({ + email: 'new@email.com', + password: 'L00pBack!', + }) + .expect(200, function (err2, token) { + token.body.should.have.properties('ttl', 'userId', 'created', 'id'); + assert.equal(token.body.userId, user.body.id); + // logout + request( + 'post', + `/users/logout?access_token=${token.body.id}`, + ).expect(204); + done(); + }); + }); + }); + + it('rejects anonymous requests to protected endpoints', function (done) { + request('get', '/CoffeeShops/greet').expect(401, function (err, res) { + assert.equal(res.body.error.code, 'AUTHORIZATION_REQUIRED'); + }); + done(); + }); + + it('makes an authenticated request', function (done) { + // create user + User.create({email: 'new@gmail.com', password: 'L00pBack!'}, function ( + err, + user, + ) { + user.email.should.be.equal('new@gmail.com'); + // login + User.login({email: 'new@gmail.com', password: 'L00pBack!'}, function ( + err2, + token, + ) { + assert.equal(typeof token, 'object'); + assert.equal(token.userId, user.id); + // authenticate user with token + request('get', `/CoffeeShops/greet?access_token=${token.id}`).expect( + 200, + function (err3, res) { + res.body.greeting.should.be.equal('Hello from this Coffee Shop'); + done(); + }, + ); + }); + }); + }); +} diff --git a/examples/lb3-application/lb3app/test/integration.js b/examples/lb3-application/lb3app/test/integration.js new file mode 100644 index 000000000000..faec37d65ddd --- /dev/null +++ b/examples/lb3-application/lb3app/test/integration.js @@ -0,0 +1,75 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-lb3-application +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; + +const assert = require('assert'); +const ExpressServer = require('../../dist/server').ExpressServer; +const CoffeeShopApp = require('../../dist/application').CoffeeShopApplication; +require('should'); + +let CoffeeShop, app; + +describe('LoopBack 3 style integration tests - boot from LB4 app', function () { + before(async function () { + app = new CoffeeShopApp(); + await app.boot(); + await app.start(); + CoffeeShop = await app.get('lb3-models.CoffeeShop'); + }); + + after(async () => { + await CoffeeShop.destroyAll({name: 'Nook Shop'}); + await app.stop(); + }); + + runTests(); +}); + +describe('LoopBack 3 style integration tests - boot from express', function () { + before(async () => { + app = new ExpressServer(); + await app.boot(); + await app.start(); + CoffeeShop = await app.lbApp.get('lb3-models.CoffeeShop'); + }); + + after(async () => { + await app.stop(); + }); + + runTests(); +}); + +function runTests() { + it('CoffeeShop.find', function (done) { + CoffeeShop.find({where: {name: 'Bel Cafe'}}, function (err, shop) { + shop[0].__data.name.should.be.equal('Bel Cafe'); + shop[0].__data.city.should.be.equal('Vancouver'); + }); + done(); + }); + + it('CoffeeShop.count', function (done) { + CoffeeShop.count({}, function (err, count) { + assert.equal(count, 6); + }); + done(); + }); + + it('CoffeeShop.create', function (done) { + CoffeeShop.create( + { + name: 'Nook Shop', + city: 'Toronto', + }, + function (err, shop) { + shop.__data.name.should.be.equal('Nook Shop'); + shop.__data.city.should.be.equal('Toronto'); + }, + ); + done(); + }); +} diff --git a/examples/lb3-application/package-lock.json b/examples/lb3-application/package-lock.json index 89b91847ad05..a1994dc5252a 100644 --- a/examples/lb3-application/package-lock.json +++ b/examples/lb3-application/package-lock.json @@ -2465,6 +2465,60 @@ "nanoid": "^2.1.0" } }, + "should": { + "version": "13.2.3", + "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", + "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", + "dev": true, + "requires": { + "should-equal": "^2.0.0", + "should-format": "^3.0.3", + "should-type": "^1.4.0", + "should-type-adaptors": "^1.0.1", + "should-util": "^1.0.0" + } + }, + "should-equal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", + "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", + "dev": true, + "requires": { + "should-type": "^1.4.0" + } + }, + "should-format": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", + "integrity": "sha1-m/yPdPo5IFxT04w01xcwPidxJPE=", + "dev": true, + "requires": { + "should-type": "^1.3.0", + "should-type-adaptors": "^1.0.1" + } + }, + "should-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", + "integrity": "sha1-B1bYzoRt/QmEOmlHcZ36DUz/XPM=", + "dev": true + }, + "should-type-adaptors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", + "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", + "dev": true, + "requires": { + "should-type": "^1.3.0", + "should-util": "^1.0.0" + } + }, + "should-util": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz", + "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==", + "dev": true + }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", diff --git a/examples/lb3-application/package.json b/examples/lb3-application/package.json index 3bf747fbba41..b5225cf6ede8 100644 --- a/examples/lb3-application/package.json +++ b/examples/lb3-application/package.json @@ -19,7 +19,7 @@ "eslint": "lb-eslint --report-unused-disable-directives .", "eslint:fix": "npm run eslint -- --fix", "pretest": "npm run build", - "test": "lb-mocha \"dist/__tests__/**/*.js\"", + "test": "lb-mocha \"dist/__tests__/**/*.js\" \"lb3app/test/*.js\"", "test:dev": "lb-mocha --allow-console-logs dist/__tests__/**/*.js && npm run posttest", "verify": "npm pack && tar xf loopback-lb3-application*.tgz && tree package && npm run clean", "migrate": "node ./dist/migrate", @@ -62,7 +62,8 @@ "@types/node": "^10.17.21", "eslint": "^7.0.0", "lodash": "^4.17.15", - "typescript": "~3.9.2" + "typescript": "~3.9.2", + "should": "^13.2.3" }, "keywords": [ "loopback", diff --git a/examples/lb3-application/src/__tests__/acceptance/lb3app.acceptance.ts b/examples/lb3-application/src/__tests__/acceptance/lb3app.acceptance.ts index 2c1db8b8e1de..aa2313d1ecc2 100644 --- a/examples/lb3-application/src/__tests__/acceptance/lb3app.acceptance.ts +++ b/examples/lb3-application/src/__tests__/acceptance/lb3app.acceptance.ts @@ -90,7 +90,7 @@ describe('CoffeeShopApplication', () => { .get(`/api/CoffeeShops/greet?access_token=${token.id}`) .expect(200); - expect(response.body.undefined).to.eql('Hello from this Coffee Shop'); + expect(response.body.greeting).to.eql('Hello from this Coffee Shop'); }); it('rejects anonymous requests to protected endpoints', async () => { diff --git a/examples/lb3-application/src/server.ts b/examples/lb3-application/src/server.ts index 5f9886cb4266..8f4b0f1f7f8a 100644 --- a/examples/lb3-application/src/server.ts +++ b/examples/lb3-application/src/server.ts @@ -19,7 +19,7 @@ const helmet = require('helmet'); export class ExpressServer { private app: express.Application; public readonly lbApp: CoffeeShopApplication; - private server?: http.Server; + public server?: http.Server; public url: String; constructor(options: ApplicationConfig = {}) { diff --git a/packages/booter-lb3app/README.md b/packages/booter-lb3app/README.md index b0bf930f82a9..60ccce5ba4b4 100644 --- a/packages/booter-lb3app/README.md +++ b/packages/booter-lb3app/README.md @@ -29,6 +29,12 @@ Register the component in Application's constructor: this.component(Lb3AppBooterComponent); ``` +By default, the LoopBack 3 models and datasources will be bond to the +application with naming conventions as: + +- binding key for datasources: `lb3-datasources.{ds name}` +- binding key for models: `lb3-models.{model name}` + ## Contributions - [Guidelines](https://github.com/strongloop/loopback-next/blob/master/docs/CONTRIBUTING.md)