From 871c1ce311bf303eb696792e7a70440fb1802e23 Mon Sep 17 00:00:00 2001 From: virkt25 Date: Fri, 17 Aug 2018 16:56:13 -0400 Subject: [PATCH 1/2] feat: implement basic user component Signed-off-by: virkt25 --- bin/setup.sh | 6 + package.json | 11 +- src/controllers/index.ts | 1 + src/controllers/user.controller.ts | 51 +++++++ src/datasources/index.ts | 6 + src/datasources/user.datasource.json | 10 ++ src/datasources/user.datasource.ts | 19 +++ src/models/index.ts | 6 + src/models/user.model.ts | 41 ++++++ src/repositories/index.ts | 6 + src/repositories/user.repository.ts | 19 +++ .../ping.controller.acceptance.ts | 2 +- test/acceptance/user.controller.acceptance.ts | 132 ++++++++++++++++++ tsconfig.json | 5 +- 14 files changed, 310 insertions(+), 5 deletions(-) create mode 100755 bin/setup.sh create mode 100644 src/controllers/user.controller.ts create mode 100644 src/datasources/index.ts create mode 100644 src/datasources/user.datasource.json create mode 100644 src/datasources/user.datasource.ts create mode 100644 src/models/index.ts create mode 100644 src/models/user.model.ts create mode 100644 src/repositories/index.ts create mode 100644 src/repositories/user.repository.ts rename test/{ => acceptance}/ping.controller.acceptance.ts (96%) create mode 100644 test/acceptance/user.controller.acceptance.ts diff --git a/bin/setup.sh b/bin/setup.sh new file mode 100755 index 000000000..1751b5ae7 --- /dev/null +++ b/bin/setup.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +CONTAINER_NAME="mongodb_c" +docker rm -f $CONTAINER_NAME +docker pull mongo:latest +docker run --name $CONTAINER_NAME -p 27017:27017 -d mongo:latest diff --git a/package.json b/package.json index 0c1d8de21..27b9e3777 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "clean": "lb-clean dist*", "commit": "git-cz", "commitmsg": "commitlint -E GIT_PARAMS", + "docker": "./bin/setup.sh", "lint": "npm run prettier:check && npm run tslint", "lint:fix": "npm run prettier:fix && npm run tslint:fix", "prettier:cli": "lb-prettier \"**/*.ts\" \"**/*.js\"", @@ -27,7 +28,7 @@ "prettier:fix": "npm run prettier:cli -- --write", "tslint": "lb-tslint", "tslint:fix": "npm run tslint -- --fix", - "pretest": "npm run clean && npm run build", + "pretest": "npm run clean && npm run build && npm run docker", "pretest:ci": "npm run build", "test": "lb-mocha --allow-console-logs \"DIST/test\"", "test:ci": "lb-mocha --allow-console-logs \"DIST/test\"", @@ -50,13 +51,16 @@ "src" ], "dependencies": { - "@loopback/context": "^0.12.2", "@loopback/boot": "^0.12.2", + "@loopback/context": "^0.12.2", "@loopback/core": "^0.11.2", "@loopback/dist-util": "^0.3.5", + "@loopback/openapi-v3": "^0.12.2", "@loopback/repository": "^0.14.2", "@loopback/rest": "^0.19.2", - "@loopback/openapi-v3": "^0.12.2" + "bcryptjs": "^2.4.3", + "isemail": "^3.1.3", + "loopback-connector-mongodb": "^1.18.1" }, "devDependencies": { "@commitlint/cli": "^7.0.0", @@ -64,6 +68,7 @@ "@commitlint/travis-cli": "^7.0.0", "@loopback/build": "^0.6.13", "@loopback/testlab": "^0.11.2", + "@types/bcryptjs": "^2.4.1", "@types/mocha": "^5.0.0", "@types/node": "^10.1.1", "commitizen": "^2.10.1", diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 2e00b9f19..5d6af1fcb 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -4,3 +4,4 @@ // License text available at https://opensource.org/licenses/MIT export * from './ping.controller'; +export * from './user.controller'; diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts new file mode 100644 index 000000000..9a156b760 --- /dev/null +++ b/src/controllers/user.controller.ts @@ -0,0 +1,51 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-shopping +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {repository} from '@loopback/repository'; +import {post, param, get, requestBody, HttpErrors} from '@loopback/rest'; +import {User} from '../models'; +import {UserRepository} from '../repositories'; +import {hash} from 'bcryptjs'; +import {promisify} from 'util'; +import * as isemail from 'isemail'; + +const hashAsync = promisify(hash); + +export class UserController { + constructor( + @repository(UserRepository) public userRepository: UserRepository, + ) {} + + @post('/users') + async create(@requestBody() user: User): Promise { + // Validate Email + if (!isemail.validate(user.email)) { + throw new HttpErrors.UnprocessableEntity('invalid email'); + } + + // Validate Password Length + if (user.password.length < 8) { + throw new HttpErrors.UnprocessableEntity( + 'password must be minimum 8 characters', + ); + } + + // Salt + Hash Password + user.password = await hashAsync(user.password, 10); + + // Save & Return Result + const savedUser = await this.userRepository.create(user); + delete savedUser.password; + return savedUser; + } + + @get('/users/{id}') + async findById(@param.path.string('id') id: string): Promise { + const user = await this.userRepository.findById(id, { + fields: {password: false}, + }); + return user; + } +} diff --git a/src/datasources/index.ts b/src/datasources/index.ts new file mode 100644 index 000000000..b2b317473 --- /dev/null +++ b/src/datasources/index.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-shopping +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './user.datasource'; diff --git a/src/datasources/user.datasource.json b/src/datasources/user.datasource.json new file mode 100644 index 000000000..77d1ede5f --- /dev/null +++ b/src/datasources/user.datasource.json @@ -0,0 +1,10 @@ +{ + "name": "user", + "connector": "mongodb", + "url": "", + "host": "127.0.0.1", + "port": 27017, + "user": "", + "password": "", + "database": "" +} diff --git a/src/datasources/user.datasource.ts b/src/datasources/user.datasource.ts new file mode 100644 index 000000000..3878edecb --- /dev/null +++ b/src/datasources/user.datasource.ts @@ -0,0 +1,19 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-shopping +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {inject} from '@loopback/core'; +import {juggler, AnyObject} from '@loopback/repository'; +const config = require('./user.datasource.json'); + +export class UserDataSource extends juggler.DataSource { + static dataSourceName = 'user'; + + constructor( + @inject('datasources.config.user', {optional: true}) + dsConfig: AnyObject = config, + ) { + super(dsConfig); + } +} diff --git a/src/models/index.ts b/src/models/index.ts new file mode 100644 index 000000000..85257409f --- /dev/null +++ b/src/models/index.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-shopping +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './user.model'; diff --git a/src/models/user.model.ts b/src/models/user.model.ts new file mode 100644 index 000000000..bd852aff2 --- /dev/null +++ b/src/models/user.model.ts @@ -0,0 +1,41 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-shopping +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class User extends Entity { + @property({ + type: 'string', + id: true, + }) + id: string; + + @property({ + type: 'string', + required: true, + }) + email: string; + + @property({ + type: 'string', + required: true, + }) + password: string; + + @property({ + type: 'string', + }) + firstname?: string; + + @property({ + type: 'string', + }) + surname?: string; + + constructor(data?: Partial) { + super(data); + } +} diff --git a/src/repositories/index.ts b/src/repositories/index.ts new file mode 100644 index 000000000..a14c8aa66 --- /dev/null +++ b/src/repositories/index.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-shopping +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './user.repository'; diff --git a/src/repositories/user.repository.ts b/src/repositories/user.repository.ts new file mode 100644 index 000000000..7104f5a7e --- /dev/null +++ b/src/repositories/user.repository.ts @@ -0,0 +1,19 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-shopping +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {DefaultCrudRepository, juggler} from '@loopback/repository'; +import {User} from '../models'; +import {inject} from '@loopback/core'; + +export class UserRepository extends DefaultCrudRepository< + User, + typeof User.prototype.id +> { + constructor( + @inject('datasources.user') protected datasource: juggler.DataSource, + ) { + super(User, datasource); + } +} diff --git a/test/ping.controller.acceptance.ts b/test/acceptance/ping.controller.acceptance.ts similarity index 96% rename from test/ping.controller.acceptance.ts rename to test/acceptance/ping.controller.acceptance.ts index 7ac308e89..f1346af2d 100644 --- a/test/ping.controller.acceptance.ts +++ b/test/acceptance/ping.controller.acceptance.ts @@ -5,7 +5,7 @@ import {createClientForHandler, supertest} from '@loopback/testlab'; import {RestServer} from '@loopback/rest'; -import {ShoppingApplication} from '../'; +import {ShoppingApplication} from '../..'; describe('PingController', () => { let app: ShoppingApplication; diff --git a/test/acceptance/user.controller.acceptance.ts b/test/acceptance/user.controller.acceptance.ts new file mode 100644 index 000000000..b3fdb2744 --- /dev/null +++ b/test/acceptance/user.controller.acceptance.ts @@ -0,0 +1,132 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-shopping +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {createClientForHandler, supertest, expect} from '@loopback/testlab'; +import {RestServer} from '@loopback/rest'; +import {ShoppingApplication} from '../..'; +import {UserRepository} from '../../src/repositories'; +import {UserDataSource} from '../../src/datasources'; + +describe('UserController', () => { + let app: ShoppingApplication; + let server: RestServer; + let client: supertest.SuperTest; + const userRepo = new UserRepository(new UserDataSource()); + + const user = { + email: 'test@loopback.io', + password: 'p4ssw0rd', + firstname: 'Example', + surname: 'User', + }; + + before(givenAnApplication); + + before(givenARestServer); + + before(async () => { + await app.boot(); + await app.start(); + }); + + before(() => { + client = createClientForHandler(server.requestHandler); + }); + + beforeEach(clearDatabase); + + after(async () => { + await app.stop(); + }); + + it('creates new user when POST /users is invoked', async () => { + const res = await client + .post('/users') + .send(user) + .expect(200); + + // Assertions + expect(res.body.email).to.equal('test@loopback.io'); + expect(res.body.firstname).to.equal('Example'); + expect(res.body.surname).to.equal('User'); + expect(res.body).to.have.property('id'); + expect(res.body).to.not.have.property('password'); + }); + + it('throws error for POST /users with a missing email', async () => { + const res = await client + .post('/users') + .send({ + password: 'p4ssw0rd', + firstname: 'Example', + surname: 'User', + }) + .expect(422); + + const errorText = JSON.parse(res.error.text); + expect(errorText.details[0].info.missingProperty).to.equal('email'); + }); + + it('throws error for POST /users with an invalid email', async () => { + const res = await client + .post('/users') + .send({ + email: 'test@loop&back.io', + password: 'p4ssw0rd', + firstname: 'Example', + surname: 'User', + }) + .expect(422); + + expect(res.body.message).to.equal('invalid email'); + }); + + it('throws error for POST /users with a missing password', async () => { + const res = await client + .post('/users') + .send({ + email: 'test@loopback.io', + firstname: 'Example', + surname: 'User', + }) + .expect(422); + + const errorText = JSON.parse(res.error.text); + expect(errorText.details[0].info.missingProperty).to.equal('password'); + }); + + it('throws error for POST /users with a string', async () => { + await client + .post('/users') + .send('hello') + .expect(415); + }); + + it('returns a user with given id when GET /user/{id} is invoked', async () => { + const newUser = await userRepo.create(user); + delete newUser.password; + // MongoDB returns an id object we need to convert to string + // since the REST API returns a string for the id property. + newUser.id = newUser.id.toString(); + + await client.get(`/users/${newUser.id}`).expect(200, newUser); + }); + + function givenAnApplication() { + app = new ShoppingApplication({ + rest: { + port: 0, + }, + }); + } + + async function givenARestServer() { + server = await app.getServer(RestServer); + } + + async function clearDatabase() { + await userRepo.deleteAll(); + } +}); diff --git a/tsconfig.json b/tsconfig.json index 8e3cda75d..c908edca3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,5 +10,8 @@ "node_modules/**", "packages/*/node_modules/**", "**/*.d.ts" - ] + ], + "compilerOptions": { + "lib": ["es2018", "dom", "esnext.asynciterable"] + } } From 666dd093f1ffb2c5d1dc13b2db8c8496a190d945 Mon Sep 17 00:00:00 2001 From: virkt25 Date: Fri, 17 Aug 2018 16:56:36 -0400 Subject: [PATCH 2/2] ci: update config for tests with mongodb Signed-off-by: virkt25 --- .travis.yml | 4 +++- appveyor.yml | 3 +++ bin/travis.sh | 15 +++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100755 bin/travis.sh diff --git a/.travis.yml b/.travis.yml index 8941ba16a..1a5c089f5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,9 +8,11 @@ os: - linux - osx +before_script: ./bin/travis.sh script: npm run test:ci env: TASK=test -install: npm install --ignore-scripts +install: npm install +services: mongodb matrix: include: diff --git a/appveyor.yml b/appveyor.yml index 5feb89359..228d50735 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,6 +3,9 @@ environment: - nodejs_version: '8' - nodejs_version: '10' +services: + - mongodb + install: - ps: Install-Product node $env:nodejs_version - node --version diff --git a/bin/travis.sh b/bin/travis.sh new file mode 100755 index 000000000..454ad6f52 --- /dev/null +++ b/bin/travis.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -e + +if [[ "${TRAVIS_OS_NAME}" == "osx" ]]; then + echo "OSX => UPDATING HOMEBREW" + brew update + echo "OSX => INSTALLING AND STARTING MONGODB" + brew install mongodb + brew services start mongodb +fi + +if [ $TASK = "test" ]; then + echo "TASK => MONGODB STARTUP DELAY" + sleep 10 +fi