I created acceptance test of controller that using memory datasource.
Before the test start, I was trying to have clean database and only add 1 user credential to login by calling endpoint since the rest of test require authenticated access with access token.
I can get the user info in getUser after created the user record from givenUser.
However, the login request can't find the credential. It seems that the helpers and client are not sharing the same memory datasource.
I am not sure what configuration setup is wrong in this case.
src/tests/fixtures/datasources/testdb.datasource.ts
import {juggler} from '#loopback/repository';
export const testdb: juggler.DataSource = new juggler.DataSource({
name: 'db',
connector: 'memory',
});
src/datasources/mongodb.datasource.ts
import {inject} from '#loopback/core';
import {juggler} from '#loopback/repository';
import * as config from './mongodb.datasource.json';
export class MongodbDataSource extends juggler.DataSource {
static dataSourceName = 'mongodb';
constructor(
#inject('datasources.config.mongodb', {optional: true})
dsConfig: object = config,
) {
super(dsConfig);
}
}
user.controller.acceptance.ts
import {Client, expect, supertest} from '#loopback/testlab';
import {ApiApplication} from '../..';
import {setupApplication} from './test-helper';
import {givenEmptyDatabase, givenUser,getUser} from '../helpers/database.helpers';
describe('UserController', () => {
let app: ApiApplication;
let client: supertest.SuperTest<supertest.Test>;;
let jwtToken: string;
before('setupApplication', async () => {
({app, client} = await setupApplication());
});
before(givenEmptyDatabase);
before(givenUser);
before(getUser);
before(async () => {
const response = await client
.post('/login')
.send({username: 'user1', password: 'password'});
jwtToken = response.body.token;
});
after(async () => {
await app.stop();
});
it('invokes GET /info without authentication', async () => {
const expectedError = {
error: {
statusCode: 401,
name: 'UnauthorizedError',
message: 'Unauthorized'
}
};
const res = await client.get('/user/info').expect(401);
expect(res.body).to.containEql(expectedError);
});
test-help.ts
import {ApiApplication} from '../..';
import {
createRestAppClient,
givenHttpServerConfig,
Client,
} from '#loopback/testlab';
import {testdb} from '../fixtures/datasources/testdb.datasource';
export async function setupApplication(): Promise<AppWithClient> {
const app = new ApiApplication();
await app.bind('datasources.config.mongodb').to({
name: 'mongodb',
connector: 'memory',
debug: true,
});
await app.boot();
await app.start();
const client = createRestAppClient(app);
return {app, client};
}
export interface AppWithClient {
app: ApiApplication;
client: Client;
}
database.helper.ts
import { UserRepository } from '../../repositories';
import { User } from '../../models';
import { testdb } from '../fixtures/datasources/testdb.datasource';
export async function givenEmptyDatabase() {
let userRepo: UserRepository;
userRepo = new UserRepository(testdb);
userRepo.deleteAll();
}
export async function givenUser() {
let userRepo = new UserRepository(testdb);
const user = {
username: 'user1',
password: 'password',
created_at: new Date('2019-08-08'),
updated_at: new Date('2019-08-08'),
}
await userRepo.create(operator);
}
export async function getUser() {
let userRepo = new UserRepository(testdb);
const users = await userRepo.find();
console.log(users);
}
It seems that the helpers and client are not sharing the same memory datasource.
Exactly.
It's important to realize that you can have multiple datasources using the same connector, for example you can one logDb using one MongoDB server and userDb connecting to another one. The same applies to memory datasources, you can have multiple datasources using the memory connector and each instance will have its own space for data.
Since you are already changing the configuration of your main mongodb datasource in the tests, my recommendation is to use that modified datasource when setting up the initial test data.
In database.helper.ts:
export async function givenEmptyDatabase(db) {
let userRepo: UserRepository;
userRepo = new UserRepository(db);
userRepo.deleteAll();
}
export async function givenUser(db) {
// ...
}
// and so on
In your test:
before(async () => {
const db = await app.get<juggler.DataSource>('datasources.config.mongodb');
await givenEmptyDatabase(db);
});
Personally, I strongly recommend use the same database in your tests as you will use in production. The memory database behaves differently from MongoDB. Certain operations may pass when storing data in memory but fail when MongoDB is used.
You can learn more about testing in LoopBack's "Best Practice" guides: Testing your application
Related
In following up from this question, I am trying to ensure the validation remains and works. However, my combined class does not validate the included fields.
For instance, I have a basic AdminCodeDTO that sepcifies the AdminCode is required, has a valid value (1-999)
import { IsNumber, Min, Max, IsDefined } from '#nestjs/class-validator';
import { ApiProperty, ApiResponseProperty } from '#nestjs/swagger';
export class AdminCodeDTO {
#ApiProperty({
description: 'Sweda Administration Code used for time tracking that is not part of a mantis.',
})
#ApiResponseProperty({ example: 5 })
#IsDefined() #IsNumber() #Min(1) #Max(999) public AdminCode: number;
constructor(AdminCode?: number) {
this.AdminCode = AdminCode;
}
}
Testing this class works, and the validation will return the errors:
import { validate } from '#nestjs/class-validator';
import { ValidationError } from '#nestjs/common';
import { AdminCodeDTO } from './admin-code-dto';
describe('AdminCodeDto', () => {
let TestDTO: AdminCodeDTO;
beforeEach( () => {
TestDTO = new AdminCodeDTO(5);
});
it('should be defined', () => {
expect(TestDTO).toBeDefined();
});
it('should have the AdminCode value set', () => {
expect(TestDTO.AdminCode).toBe(5);
});
it('should allow creation with an empty constructor', () => {
expect(new AdminCodeDTO()).toBeDefined();
});
it('should generate the DTO errors', async () => {
const DTOValidCheck: AdminCodeDTO = new AdminCodeDTO();
const Errors: Array<ValidationError> = await validate(DTOValidCheck);
expect(Errors.length).toBe(1);
expect(Errors[0].constraints['isDefined']).toBe('AdminCode should not be null or undefined');
expect(Errors[0].constraints['isNumber']).toBe('AdminCode must be a number conforming to the specified constraints');
expect(Errors[0].constraints['max']).toBe('AdminCode must not be greater than 999');
expect(Errors[0].constraints['min']).toBe('AdminCode must not be less than 1');
});
});
To then build a simple DTO combining 2 fields to do the testing, I create a description DTO as well, to add that field for this simple example.
import { IsDefined, IsString, MaxLength, MinLength } from '#nestjs/class-validator';
import { ApiProperty, ApiResponseProperty } from '#nestjs/swagger';
export class DescriptionDTO {
#ApiProperty({
description: '',
minLength: 3,
maxLength: 20
})
#ApiResponseProperty({ example: 'Sick Day' })
#IsDefined() #IsString() #MaxLength(20) #MinLength(3) public Description: string;
constructor(Description?: string) {
this.Description = Description;
}
}
I then use the IntersectionType of #nestjs/swagger, to combine the AdminCodeDTO, with a new description field for the payload.
import { IsDefined, IsString, MaxLength, MinLength } from '#nestjs/class-validator';
import { ApiProperty, ApiResponseProperty, IntersectionType} from '#nestjs/swagger';
import { AdminCodeDTO } from './admin-code-dto';
export class AdmininstrationCodesDTO extends IntersectionType(
AdminCodeDTO,
DescriptionDTO
)
{
constructor(AdminCode?: number, Description?: string) {
this.AdminCode = AdminCode;
this.Description = Description;
}
My test however, while all the columns are defined, the validation does not work.
import { AdmininstrationCodesDTO } from './admininstration-codes-dto';
describe('AdmininstrationCodesDTO', () => {
let TestDTO: AdmininstrationCodesDTO;
beforeEach( () => {
TestDTO = new AdmininstrationCodesDTO(77, 'Test Admin Code');
})
it('should be defined', () => {
expect(TestDTO).toBeDefined();
});
it('should be defined when launched without parameters', () => {
expect(new AdmininstrationCodesDTO()).toBeDefined();
})
it.each([
['AdminCode', 77],
['Description', 'Test Admin Code'],
])('should have the proper field {%s} set to be %d', (FieldName, Expected) => {
expect(FieldName in TestDTO).toBe(true);
expect(TestDTO[FieldName]).toBe(Expected);
});
// This test fails as the validation settings are not enforced. Working on any of the DTOs directly though, the validation is confirmed.
it('should generate the DTO errors', async () => {
const TestDTO: AdmininstrationCodesDTO = new AdmininstrationCodesDTO();
const Errors: Array<ValidationError> = await validate(TestDTO, );
expect(Errors.length).toBe(8);
});
});
EDIT: This also causes a problem in my Swagger UI documentation, where this method now prevents my request schemas from showing the data. When I define my fields directly in the DTO (without IntersectionType) the fields show up in the request schema for Swagger. I have the CLI functions enabled in the project.json (NX monorepo).
As found out from your GitHub Issue (thank you for that by the way) you were using #nestjs/class-validator and #nestjs/class-transformer for the validator and transformer packages. #nestjs/mapped-types uses the original class-valdiator and class-transformer packages and these packages use an internal metadata storage device rather than the full Reflect API and metadata storage, so when Nest tried to copy over the metadata from class-validator there was none found because of the use of #nestjs/class-validator, which ended up in having no metadata present for the IntersectionType request
In the test/posts/posts.e2e-spec.ts file
import { INestApplication } from '#nestjs/common';
import { TypeOrmModule } from '#nestjs/typeorm';
import { Test, TestingModule } from '#nestjs/testing';
import request = require('supertest');
import { PostsModule } from '../../src/posts/posts.module';
describe('Posts (e2e)', () => {
const posts = {
id: 1,
name: 'FirstPost #1',
};
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
...
}),
PostModule,
],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
describe('post', () => {
it('should retrieve all post data', async () => {
request(app.getHttpServer())
.post('/graphql')
.send({
query:
`{findPosts() {
name
}}`,
})
.expect(200)
.expect((res) => {
console.log(res.body.data)
expect(res.body.data.post.length).toEqual(posts.length)
})
})
})
});
I created migration and inserted data into database first, then run this test, it can't go to the expect items. Even set console log I can't see anything in the output.
So maybe the /graphql can't be access in this way? I can access the endpoint from browser as http://localhost:3000/graphql.
If import supertest as
import * as request from 'supertest';
In the line request it showed:
This expression is not callable. Type ‘typeof supertest’ has no call signatures.
The version of them:
supertest: 6.1.3
#types/supertest: 2.0.11
Check out this very useful link https://github.com/jmcdo29/testing-nestjs/tree/main/apps/graphql-sample. It explains a lot of things regarding tests including graphql nestjs testing along with sample application
The Nestjs module system is great, but I'm struggling to figure out how to take full advantage of it in a Serverless setting.
I like the approach of writing my domain logic in *.service.ts files, while using *.controller.ts files to take care of non-business related tasks such as validating an HTTP request body and converting to a DTO before invoking methods in a service.
I found the section on Serverless in the nestjs docs and determined that for my specific use-case, I need to use the "standalone application feature".
I created a sample nestjs app here to illustrate my problem.
The sample app has a simple add() function to add two numbers. I use class-validator for validation on the AddDto class.
// add.dto.ts
import { IsNumber } from 'class-validator'
export class AddDto {
#IsNumber()
public a: number;
#IsNumber()
public b: number;
}
And then, via some Nestjs magic, I am able to get built-in validation using the AddDto inside my controller by doing the following:
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Use `ValidationPipe()` for auto-validation in controllers
app.useGlobalPipes(
new ValidationPipe({ transform: true })
)
await app.listen(3000);
}
// app.controller.ts
#Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
#Post('add')
add(#Body() dto: AddDto): number {
// Request body gets auto validated and converted
// to an instance of `AddDto`, sweet!
return this.appService.add(dto.a, dto.b);
}
}
// app.service.ts
#Injectable()
export class AppService {
add(a: number, b: number): number {
return a + b
}
}
So far, so good. The problem now arises when using this in AWS with a Lambda function, namely:
I want to re-use the business logic in app.service.ts
I want to re-use built in validation that happens when making an HTTP request to the app, such as in the example above.
I want to use the standalone app feature so I don't have to spin up an entire nest server in Lambda
The docs hint on this being a problem:
Be aware that NestFactory.createApplicationContext does not wrap controller methods with enhancers (guard, interceptors, etc.). For this, you must use the NestFactory.create method.
For example, I have a lambda that receives messages from AWS EventBridge. Here's a snippet from the sample app:
// standalone-app.ts
interface IAddCommand {
a: number;
b: number;
}
export const handler = async (
event: EventBridgeEvent<'AddCommand', IAddCommand>,
context: any
) => {
const appContext = await NestFactory.createApplicationContext(AppModule);
const appService = appContext.get(AppService);
const { a, b } = event.detail;
const sum = appService.add(a, b)
// do work on `sum`, like cache the result, etc...
return sum
};
// lambda-handler.js
const { handler } = require('./dist/standalone-app')
handler({
detail: {
a: "1", // is a string, should be a number
b: "2" // is a string, should be a number
}
})
.then(console.log) // <--- prints out "12" ("1" + "2") instead of "3" (1 + 2)
I don't get "free" validation of the event's payload in event.detail like I do with #Body() dto: AddDto when making a HTTP POST request to /add. Preferentially, the code would throw a validation error in the above example. Instead, I get an answer of "12" -- a false positive.
Hopefully, this illustrates the crux of my problem. I still want to validate the payload of the event before calling appService.add(a, b), but I don't want to write custom validation logic that already exists on the controller in app.controller.ts.
Ideas? Anyone else run into this before?
It occurred to me while writing this behemoth of a question that I can simply use class-validator and class-transformer in my Lambda handler.
import { validateOrReject } from 'class-validator'
import { plainToClass } from 'class-transformer'
import { AddDto } from 'src/dto/add.dto'
export const handler = async (event: any, context: any) => {
const appContext = await NestFactory.createApplicationContext(AppModule);
const appService = appContext.get(AppService);
const data = getPayloadFromEvent(event)
// Convert raw data to a DTO
const dto: AddDto = plainToClass(AddDto, data)
// Validate it!
await validateOrReject(dto)
const sum = appService.add(dto.a, dto.b)
// do work on `sum`...
}
It's not as "free" as using app.useGlobalPipes(new ValidationPipe()), but only involves a few extra lines of code.
It worked for me with the following lambda file for nestjs.
import { configure as serverlessExpress } from '#vendia/serverless-express';
import { NestFactory } from '#nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '#nestjs/common';
let cachedServer;
export const handler = async (event, context) => {
if (!cachedServer) {
const nestApp = await NestFactory.create(AppModule);
await nestApp.useGlobalPipes(new ValidationPipe());
await nestApp.init();
cachedServer = serverlessExpress({
app: nestApp.getHttpAdapter().getInstance(),
});
}
return cachedServer(event, context);
};
I installed the Vuex-ORM Graphql Plugin into an existing Nuxt project with Laravel/GraphQL API, so that I could try avoiding using the Apollo Cache. In one of my components though, I'm running:
<script>
import Notification from '~/data/models/notification';
export default {
computed: {
notifications: () => Notification.all()
},
async mounted () {
await Notification.fetch();
}
}
</script>
however I'm receiving the error [vuex] unknown action type: entities/notifications/fetch.
I looked through the debug log and found several available getters (entities/notifications/query, entities/notifications/all, entities/notifications/find, and entities/notifications/findIn). I tried running await Notification.all() in the mounted method which removed the error, however looking in Vuex the Notifications data object is empty.
Here is the rest of my setup:
nuxt.config.js
plugins: [
'~/plugins/vuex-orm',
'~/plugins/graphql'
],
plugins/vuex-orm.js
import VuexORM from '#vuex-orm/core';
import database from '~/data/database';
export default ({ store }) => {
VuexORM.install(database)(store);
};
plugins/graphql.js
/* eslint-disable import/no-named-as-default-member */
import VuexORM from '#vuex-orm/core';
import VuexORMGraphQL from '#vuex-orm/plugin-graphql';
import { HttpLink } from 'apollo-link-http';
import fetch from 'node-fetch';
import CustomAdapter from '~/data/adapter';
import database from '~/data/database';
// The url can be anything, in this example we use the value from dotenv
export default function ({ app, env }) {
const apolloClient = app?.apolloProvider?.defaultClient;
const options = {
adapter: new CustomAdapter(),
database,
url: env.NUXT_ENV_BACKEND_API_URL,
debug: process.env.NODE_ENV !== 'production'
};
if (apolloClient) {
options.apolloClient = apolloClient;
} else {
options.link = new HttpLink({ uri: options.url, fetch });
}
VuexORM.use(VuexORMGraphQL, options);
};
/data/adapter.js
import { DefaultAdapter, ConnectionMode, ArgumentMode } from '#vuex-orm/plugin-graphql';
export default class CustomAdapter extends DefaultAdapter {
getConnectionMode () {
return ConnectionMode.PLAIN;
}
getArgumentMode () {
return ArgumentMode.LIST;
}
};
/data/database.js
import { Database } from '#vuex-orm/core';
// import models
import Notification from '~/data/models/notification';
import User from '~/data/models/user';
const database = new Database();
database.register(User);
database.register(Notification);
export default database;
/data/models/user.js
import { Model } from '#vuex-orm/core';
import Notification from './notification';
export default class User extends Model {
static entity = 'users';
static eagerLoad = ['notifications'];
static fields () {
return {
id: this.attr(null),
email: this.string(''),
first_name: this.string(''),
last_name: this.string(''),
// relationships
notifications: this.hasMany(Notification, 'user_id')
};
}
};
/data/models/notification.js
import { Model } from '#vuex-orm/core';
import User from './user';
export default class Notification extends Model {
static entity = 'notifications';
static fields () {
return {
id: this.attr(null),
message: this.string(''),
viewed: this.boolean(false),
// relationships
user: this.belongsTo(User, 'user_id')
};
}
};
package.json
"#vuex-orm/plugin-graphql": "^1.0.0-rc.41"
So in a Hail Mary throw to get this working, I ended up making a couple of changes that actually worked!
If other people come across this having similar issues, here's what I did...
In my nuxt.config.js, swapped the order of the two plugins to this:
plugins: [
'~/plugins/graphql',
'~/plugins/vuex-orm',
],
In my graphql.js plugin, I rearranged the order of the options to this (database first, followed by adapter):
const options = {
database,
adapter: new CustomAdapter(),
url: env.NUXT_ENV_BACKEND_API_URL,
debug: process.env.NODE_ENV !== 'production'
};
I made a register page that use restClient to send a POST to /users api.
But my problem is that the only way to send a POST is to be logged first as I receive this error log from the restClient :
'Could not find stored JWT and no authentication strategy was given'
Is there a way to desactivate the authentication middleware for a specific api call ?
// registerActions.js
import { CREATE } from 'admin-on-rest'
export const USER_REGISTER = 'AOR/USER_REGISTER'
export const USER_REGISTER_LOADING = 'AOR/USER_REGISTER_LOADING'
export const USER_REGISTER_FAILURE = 'AOR/USER_REGISTER_FAILURE'
export const USER_REGISTER_SUCCESS = 'AOR/USER_REGISTER_SUCCESS'
export const userRegister = (data, basePath) => ({
type: USER_REGISTER,
payload: { data: { email: data.username, ...data } },
meta: { resource: 'users', fetch: CREATE, auth: true },
})
//registerSaga.js
import { put, takeEvery, all } from 'redux-saga/effects'
import { push } from 'react-router-redux'
import { showNotification } from 'admin-on-rest'
import {
USER_REGISTER,
USER_REGISTER_LOADING,
USER_REGISTER_SUCCESS,
USER_REGISTER_FAILURE
} from './registerActions'
function* registerSuccess() {
yield put(showNotification('Register approved'))
yield put(push('/'))
}
function* registerFailure({ error }) {
yield put(showNotification('Error: register not approved', 'warning'))
console.error(error)
}
export default function* commentSaga() {
yield all([
takeEvery(USER_REGISTER_SUCCESS, registerSuccess),
takeEvery(USER_REGISTER_FAILURE, registerFailure),
])
}
You'll probably have to make your own feathers client and explicitly bypass the call to authenticate for this specific request
You can also write a rest wrappper this will intercept the call for this particular case and bypass auth
https://marmelab.com/admin-on-rest/RestClients.html#decorating-your-rest-client-example-of-file-upload
So something like below
const restWrapper = requestHandler => (type, resource, params) => {
import { fetchUtils } from 'admin-on-rest';
if (type === 'CREATE' && resource === 'users') {
return fetchUtils.fetchJson(url, params)
.then((response) => {
const {json} = response;
return { data: json };
})
}
Eliminates the need of rewriting an entire Rest Client when you only want to override the default behaviour for a single case