Skip to content
141 changes: 141 additions & 0 deletions SPIKE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Run LB3 tests from LB4 when LB3 is mounted on the LB4 app

Since LoopBack 4 offers a way to mount LoopBack 3 applications on a LoopBack 4
project with the use of
[`@loopback/booter-lb3app`](https://github.com/strongloop/loopback-next/tree/master/packages/booter-lb3app),
there should also be a way for users to run their LoopBack 3 tests as part of
LoopBack 4's `npm test` command.

We want the LoopBack 3 tests to use the LoopBack 4 server rather than the
LoopBack 3 application. This spike aims to test running both acceptance and
integration LoopBack 3 tests.

## All Tests

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.

## Acceptance Tests

First, move any LoopBack 3 test dependencies to package.json's devDependencies
and run:

```sh
npm install
```

In your test file:

- Update to use the LB4 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 request(verb, url) {
// use the LB4 express server
return supertest(app.server)
[verb](url)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
.expect('Content-Type', /json/);
}
```

- Boot and start the LB4 app in your before hook, and stop the app in the after
hook:

```ts
describe('LoopBack 3 style tests', function () {
before(async function () {
app = new ExpressServer();
await app.boot();
await app.start();
});

after(async () => {
await app.stop();
});

// your tests here
});
```

- Example of this use can be seen in
[`examples/lb3-application/lb3app/test/acceptance.js`](https://github.com/strongloop/loopback-next/blob/spike/lb3test/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.

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.

## 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, `getSync()` can
be used as follows:

```ts
describe('LoopBack 3 style integration tests', function () {
let app;
let CoffeeShop;

before(async function () {
app = new ExpressServer();
await app.boot();
await app.start();
});

before(() => {
// follow the syntax: lb3-models.{ModelName}
CoffeeShop = app.lbApp.getSync('lb3-models.CoffeeShop');
});

after(async () => {
await app.stop();
});

// your tests here
});
```

Alternatively, `get()` can also be used in to retrieve the model asynchronously:

```ts
before(async () => {
// follow the syntax: lb3-models.{ModelName}
CoffeeShop = await app.lbApp.get('lb3-models.CoffeeShop');
});
```

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/blob/spike/lb3test/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/blob/spike/lb3test/examples/lb3-application/lb3app/test/authentication.js).
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,6 @@ module.exports = function (CoffeeShop) {
};
CoffeeShop.remoteMethod('greet', {
http: {path: '/greet', verb: 'get'},
returns: {type: 'string'},
returns: {arg: 'greeting', type: 'string'},
});
};
72 changes: 72 additions & 0 deletions examples/lb3-application/lb3app/test/acceptance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// 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');
require('should');

let app;

function request(verb, url) {
// use the original app's server
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', function () {
before(async function () {
app = new ExpressServer();
await app.boot();
await app.start();
});

after(async () => {
await app.stop();
});

context('basic REST calls for LoopBack 3 application', () => {
it('creates and finds a CoffeeShop', function (done) {
request('post', '/api/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', '/api/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();
});
});

it('gets external route in application', function (done) {
request('get', '/ping').expect(200, function (err, res) {
assert.equal(res.text, 'pong');
done();
});
});
});
});
99 changes: 99 additions & 0 deletions examples/lb3-application/lb3app/test/authentication.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// 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');
require('should');

let app;

function request(verb, url) {
// use the original app's server
return supertest(app.server)
[verb](url)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
.expect('Content-Type', /json/);
}

describe('LoopBack 3 authentication', function () {
before(async function () {
app = new ExpressServer();
await app.boot();
await app.start();
});

after(async () => {
await app.stop();
});

context('authentication', () => {
let User;

before(() => {
User = lb3App.models.User;
});

it('creates a User and logs them in and out', function (done) {
request('post', '/api/users')
.send({email: 'new@email.com', password: 'L00pBack!'})
.expect(200, function (err, user) {
assert.equal(user.body.email, 'new@email.com');
request('post', '/api/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);
request(
'post',
`/api/users/logout?access_token=${token.body.id}`,
).expect(204);
done();
});
});
});

it('rejects anonymous requests to protected endpoints', function (done) {
request('get', '/api/CoffeeShops/greet').expect(401, function (err, res) {
assert.equal(res.body.error.code, 'AUTHORIZATION_REQUIRED');
});
done();
});

it('makes an authenticated request', function (done) {
User.create({email: 'new@gmail.com', password: 'L00pBack!'}, function (
err,
user,
) {
user.email.should.be.equal('new@gmail.com');
User.login({email: 'new@gmail.com', password: 'L00pBack!'}, function (
err2,
token,
) {
assert.equal(typeof token, 'object');
assert.equal(token.userId, user.id);
request(
'get',
`/api/CoffeeShops/greet?access_token=${token.id}`,
).expect(200, function (err3, res) {
res.body.greeting.should.be.equal('Hello from this Coffee Shop');
done();
});
});
});
});
});
});
58 changes: 58 additions & 0 deletions examples/lb3-application/lb3app/test/integration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// 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;
require('should');

describe('LoopBack 3 style integration tests', function () {
let app;
let CoffeeShop;

before(async function () {
app = new ExpressServer();
await app.boot();
await app.start();
});

before(() => {
CoffeeShop = app.lbApp.getSync('lb3-models.CoffeeShop');
});

after(async () => {
await app.stop();
});

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, 5);
});
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();
});
});
Loading