diff --git a/package-lock.json b/package-lock.json index 1c148587..ecb8d93e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2897,6 +2897,14 @@ "@types/node": "*" } }, + "@types/nodemailer": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.2.2.tgz", + "integrity": "sha512-vDbSSe3+bBXYgibKs8duOrH7bhAv1hMHl3Vtdr3KmXZWLfi5WtIg3X6D/+K4SMK1RbNlm5QZBQCOldST/sVjjg==", + "requires": { + "@types/node": "*" + } + }, "@types/normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", @@ -6403,9 +6411,9 @@ "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=" }, "handlebars": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.2.tgz", - "integrity": "sha512-cIv17+GhL8pHHnRJzGu2wwcthL5sb8uDKBHvZ2Dtu5s1YNt0ljbzKbamnc+gr69y7bzwQiBdr5+hOpRd5pnOdg==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", + "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -7016,15 +7024,24 @@ "integrity": "sha512-nDUtj0ltIt08tGi2VWSpSzNNFye0v3YSe9lX3lIqLTuVvvRiYCvs4QQBSHo0eomFYw1wlUuofurUAlTm+vHnXg==" }, "https-proxy-agent": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", - "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", "dev": true, "requires": { - "agent-base": "^4.1.0", + "agent-base": "^4.3.0", "debug": "^3.1.0" }, "dependencies": { + "agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "dev": true, + "requires": { + "es6-promisify": "^5.0.0" + } + }, "debug": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", @@ -7035,9 +7052,9 @@ } }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true } } @@ -9578,9 +9595,9 @@ "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" }, "neo-async": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.0.tgz", - "integrity": "sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", "dev": true }, "netmask": { @@ -9800,6 +9817,11 @@ "asap": "~2.0.3" } }, + "nodemailer": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.3.1.tgz", + "integrity": "sha512-j0BsSyaMlyadEDEypK/F+xlne2K5m6wzPYMXS/yxKI0s7jmT1kBx6GEKRVbZmyYfKOsjkeC/TiMVDJBI/w5gMQ==" + }, "nodemon": { "version": "1.18.10", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.18.10.tgz", @@ -12899,20 +12921,20 @@ "integrity": "sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw==" }, "uglify-js": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.4.tgz", - "integrity": "sha512-GpKo28q/7Bm5BcX9vOu4S46FwisbPbAmkkqPnGIpKvKTM96I85N6XHQV+k4I6FA2wxgLhcsSyHoNhzucwCflvA==", + "version": "3.6.9", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.9.tgz", + "integrity": "sha512-pcnnhaoG6RtrvHJ1dFncAe8Od6Nuy30oaJ82ts6//sGSXOP5UjBMEthiProjXmMNHOfd93sqlkztifFMcb+4yw==", "dev": true, "optional": true, "requires": { - "commander": "~2.20.0", + "commander": "~2.20.3", "source-map": "~0.6.1" }, "dependencies": { "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, "optional": true }, diff --git a/package.json b/package.json index 7581f589..9b13e364 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@types/multer": "^1.3.7", "@types/mysql": "^2.15.6", "@types/nodegit": "^0.24.11", + "@types/nodemailer": "^6.2.2", "@types/phone": "^1.0.3", "@types/reflect-metadata": "^0.1.0", "@types/sequelize": "^4.28.4", @@ -58,6 +59,7 @@ "mysql": "^2.17.1", "mysql2": "^1.6.5", "nodegit": "^0.26.2", + "nodemailer": "^6.3.1", "phone": "^2.3.18", "reflect-metadata": "^0.1.13", "sequelize": "^5.16.0", diff --git a/src/app/api/accounts/accounts.service.ts b/src/app/api/accounts/accounts.service.ts index 07b2fb8f..fd38d0ac 100644 --- a/src/app/api/accounts/accounts.service.ts +++ b/src/app/api/accounts/accounts.service.ts @@ -8,12 +8,12 @@ class AccountService extends CrudService { super(new Repo(AccountModel)); } - public deleteAccount(user_id) { - return this.delete({ user_id }); - } - public createAccount(user_id) { - return this.create({ user_id }); - } + // public deleteAccount(user_id) { + // return this.delete({ user_id }); + // } + // public createAccount(user_id) { + // return this.create({ user_id }); + // } public async getEntityByUserId(user_id) { const entity = await this.one({ user_id }); diff --git a/src/app/api/favorites/favorites.model.ts b/src/app/api/favorites/favorites.model.ts index e41d51d1..94ed844e 100644 --- a/src/app/api/favorites/favorites.model.ts +++ b/src/app/api/favorites/favorites.model.ts @@ -1,27 +1,15 @@ import { Constants } from '@core/helpers'; import { BaseModel, Entity, Field } from '@lib/mongoose'; -import { Types } from 'mongoose'; +import { Types, Schema } from 'mongoose'; @Entity(Constants.Schemas.favorites) export class FavoritesSchema { - @Field({ lowercase: false }) public user_id: Types.ObjectId; - @Field({ lowercase: false }) public item_id: Types.ObjectId; + // TODO: constructor(schema: Schema) { } + @Field() public user_id: Types.ObjectId; + @Field() public item_id: Types.ObjectId; @Field({ enum: [Constants.Schemas.meals, Constants.Schemas.MENUS] }) public type: string; - - // @Virtual('assigned') assignedVirtual() { - // return { - // ref: this.type, - // localField: 'item_id', - // foreignField: '_id', - // justOne: false - // }; - // } - - // get test(): Partial { - // return this.type; - // } } export const FavoritesModel = BaseModel(FavoritesSchema); FavoritesModel.schema.virtual('item', { diff --git a/src/app/api/favorites/favorites.routes.ts b/src/app/api/favorites/favorites.routes.ts index 322ac6f5..51d2e3a5 100644 --- a/src/app/api/favorites/favorites.routes.ts +++ b/src/app/api/favorites/favorites.routes.ts @@ -4,9 +4,10 @@ import { Delete, Get, Post, Router } from '@lib/methods'; import { translate } from '@lib/translation'; import { Request, Response } from 'express'; import { FavoritesRepo } from './favorites.repo'; +import { CrudRouter } from '@shared/crud'; @Router(Constants.Endpoints.favorites) -export class FavoritesRouter { +export class FavoritesRouter extends CrudRouter { private repo = FavoritesRepo; @Post(':type', Auth.isAuthenticated) diff --git a/src/app/api/meals/meals.spec.ts b/src/app/api/meals/meals.spec.ts index 0e7a2f2a..07b7d0cb 100644 --- a/src/app/api/meals/meals.spec.ts +++ b/src/app/api/meals/meals.spec.ts @@ -1,9 +1,14 @@ import { superAgent } from '@test/index'; import { Constants, NetworkStatus } from '@core/helpers'; - +global.console = { + ...global.console, + log: jest.fn(), + info: jest.fn(), + error: jest.fn(), +}; describe('#Get All', () => { it('Should return data without token', async () => { - const res = await (await superAgent).post(`/api/${Constants.Endpoints.MEALS}`); + const res = await (await superAgent).get(`/api/${Constants.Endpoints.MEALS}`); expect(res.body.data).toBeInstanceOf(Array); expect(res.status).toBe(NetworkStatus.OK); }); diff --git a/src/app/api/menus/menus.spec.ts b/src/app/api/menus/menus.spec.ts index 4caa48b3..77b4806f 100644 --- a/src/app/api/menus/menus.spec.ts +++ b/src/app/api/menus/menus.spec.ts @@ -2,9 +2,10 @@ import { superAgent } from '@test/index'; import { Constants, NetworkStatus } from '@core/helpers'; describe('#Get All', () => { - it('Should return data without token', async () => { - const res = await (await superAgent).post(`/api/${Constants.Endpoints.MENUS}`); + it('Should return data without token', async (done) => { + const res = await (await superAgent).get(`/api/${Constants.Endpoints.MENUS}`); expect(res.body.data).toBeInstanceOf(Array); expect(res.status).toBe(NetworkStatus.OK); + done(); }); }); diff --git a/src/app/api/portal/portal.routes.ts b/src/app/api/portal/portal.routes.ts index 521f0cf9..27d4c462 100644 --- a/src/app/api/portal/portal.routes.ts +++ b/src/app/api/portal/portal.routes.ts @@ -3,25 +3,72 @@ import { Post, Router } from '@lib/methods'; import { translate } from '@lib/translation'; import { Request, Response } from 'express'; import usersService from '@api/users/users.service'; +import { UsersSchema } from '@api/users'; +import { Body } from '@lib/mongoose'; -// TODO: create the profile / acount strategy +// TODO: create the profile / account strategy @Router(Constants.Endpoints.PORTAL) export class PortalRoutes { - @Post(`login/${Constants.Endpoints.USERS}`) - public async loginUser(req: Request, res: Response) { - const { username, password } = req.body; + @Post(`login`) + public async login(req: Request, res: Response) { + const { username, password } = req.body as Body; const entity = await usersService.one({ username }); if (!!entity) { const isPasswordEqual = await entity.comparePassword(password); if (isPasswordEqual) { + // TODO: replace the response with login response and not user info const response = new SuccessResponse(entity, translate('success')); - response.token = tokenService.generateToken({ id: entity.id }); + response.token = tokenService.generateToken({ + id: entity.id, + role: entity.role + }); return res.status(response.code).json(response); } } throw new ErrorResponse(translate('wrong_credintals')); } + public async forgotPassword(req: Request, res: Response) { + const { username } = req.body as Body; + const entity = await throwIfNotExist({ username }); + // sendEmail(entity.email); + } + +} + +async function throwIfNotExist(query: Partial>) { + const entity = await usersService.one(query); + if (!!entity) { + return entity; + } + throw new ErrorResponse(translate('not_exist')); } + +import nodemailer from 'nodemailer'; + +// const transporter = nodemailer.createTransport({ +// service: 'gmail', +// auth: { +// } +// }); + +// const message = { +// from: 'ezzabuzaid@gmail.com', +// to: 'ezzabuzaid@hotmail.com', +// subject: 'Nodemailer is unicode friendly ✔', +// text: 'Hello to myself!', +// html: '

Hello to myself!

' +// }; + +// transporter.sendMail(message, (err, info) => { +// if (err) { +// console.log('Error occurred. ' + err.message); +// return process.exit(1); +// } + +// console.log('Message sent: %s', info.messageId); +// // Preview only available when sending through an Ethereal account +// console.log('Preview URL: %s', nodemailer.getTestMessageUrl(info)); +// }); diff --git a/src/app/api/portal/portal.spec.ts b/src/app/api/portal/portal.spec.ts index 68838cb0..3a11066a 100644 --- a/src/app/api/portal/portal.spec.ts +++ b/src/app/api/portal/portal.spec.ts @@ -1,42 +1,49 @@ -import { UsersSchema } from '@api/users'; +import { UsersSchema, ERoles } from '@api/users'; import { Body } from '@lib/mongoose'; import { superAgent } from '@test/index'; import { Constants, NetworkStatus } from '@core/helpers'; -import { UserFixture } from '@test/fixture'; +import { UserFixture as userFixture, getUri } from '@test/fixture'; +import { AppUtils } from '@core/utils'; -const ENDPOINT = `/api/${Constants.Endpoints.PORTAL}/login/${Constants.Endpoints.USERS}/`; +const ENDPOINT = getUri(`${Constants.Endpoints.PORTAL}/login`); -let user: UserFixture = null; +const mockUser = { + password: '123456789', + username: 'portalLogin', + email: 'portal@login.com', + mobile: '0792807794', + profile: null, + role: ERoles.SUPERADMIN +} as Body; +let userFixture: userFixture = null; beforeAll(async () => { - const body = { - password: '123456789', - username: 'portalLogin', - email: 'portal@login.com', - mobile: '0792807794' - } as Body; const req = (await superAgent).post(ENDPOINT); - const res = await req.send(body); - user = res.body.data; + const res = await req.send(mockUser); + userFixture = res.body.data; }); describe('Login should fail if..', () => { it('`Request with non existing user`', async () => { - const body = { - password: '123456789', - username: 'portalLogin' - } as Body; const req = (await superAgent).post(ENDPOINT); - const res = await req.send(body); + const res = await req.send({ username: 'fakeUsername', password: 'fakePassword' }); + expect(res.status).toBe(NetworkStatus.BAD_REQUEST); + }); + it('The Username was wrong', async () => { + const req = (await superAgent).post(ENDPOINT); + const res = await req.send(AppUtils.assignObject({}, mockUser, { username: 'fakeUsername' })); + expect(res.status).toBe(NetworkStatus.BAD_REQUEST); + }); + it('The password was wrong', async () => { + const req = (await superAgent).post(ENDPOINT); + const res = await req.send(AppUtils.assignObject({}, mockUser, { password: 'fakePassword' })); expect(res.status).toBe(NetworkStatus.BAD_REQUEST); }); - it.todo('The Username was wrong'); - it.todo('The password was wrong'); it.todo('User trying to login with an old password'); it.todo('User has more than three session'); }); describe('Login should success when', () => { it.todo('Token is valid'); - it.todo('Token has the appropriate schem'); + it.todo('Token has the appropriate schema'); }); diff --git a/src/app/api/profiles/profile.service.ts b/src/app/api/profiles/profile.service.ts new file mode 100644 index 00000000..809c05b7 --- /dev/null +++ b/src/app/api/profiles/profile.service.ts @@ -0,0 +1,9 @@ +import accountsService from '@api/accounts/accounts.service'; + +export class ProfileService { + constructor() { } + + public update() { + // accountsService.update(); + } +} diff --git a/src/app/api/users/users.model.ts b/src/app/api/users/users.model.ts index 335d3eba..fd6c15a6 100644 --- a/src/app/api/users/users.model.ts +++ b/src/app/api/users/users.model.ts @@ -2,11 +2,27 @@ import { HashService, Constants } from '@core/helpers'; import { BaseModel, Entity, Field } from '@lib/mongoose'; import { ValidationPatterns } from '@shared/common'; import { parsePhoneNumberFromString } from 'libphonenumber-js'; -import { Query } from 'mongoose'; +import { Query, Schema } from 'mongoose'; import { translate } from '@lib/translation'; +import { AppUtils } from '@core/utils'; + +export enum ERoles { + SUPERADMIN, + ADMIN, + CLIENT, + CUSTOMER, +} @Entity(Constants.Schemas.USERS) export class UsersSchema { + @Field({ enum: Object.values(ERoles) }) public role: ERoles; + @Field({ + default: {}, + set: (value) => { + console.log(value); + return AppUtils.isNullOrUndefined(value) ? {} : value; + } + }) public profile: {}; // TODO: update this field to be ProfileSchema instead @Field({ pure: true, required: true }) public password: string; @Field({ match: [ValidationPatterns.NoSpecialChar, translate('no_speical_char')], diff --git a/src/app/api/users/users.service.ts b/src/app/api/users/users.service.ts index 0122ce3b..ed26b4df 100644 --- a/src/app/api/users/users.service.ts +++ b/src/app/api/users/users.service.ts @@ -7,19 +7,12 @@ import { usersRepo } from '.'; class UserService extends CrudService { constructor() { super(usersRepo, { - unique: [{ attr: 'username' }, { attr: 'email' }], + unique: ['username', 'email', 'password'], create: { async pre(entity) { + // TODO: Instead of invoke this function in hook invoke it in password setter await entity.hashUserPassword(); }, - async post(entity) { - await accountsService.createAccount(entity.id); - } - }, - delete: { - async post(entity) { - await accountsService.deleteAccount(entity.id); - } } }); } diff --git a/src/app/api/users/users.spec.ts b/src/app/api/users/users.spec.ts index 35f2e96a..d82b748f 100644 --- a/src/app/api/users/users.spec.ts +++ b/src/app/api/users/users.spec.ts @@ -1,14 +1,23 @@ import '@test/index'; import { superAgent } from '@test/supertest'; -import { createUser, deleteUser, UserFixture } from '@test/fixture'; +import { createUser, deleteUser, getUri } from '@test/fixture'; import { Constants, NetworkStatus } from '@core/helpers'; import { Body } from '@lib/mongoose'; -import { UsersSchema } from './users.model'; +import { UsersSchema, ERoles } from './users.model'; + +const ENDPOINT = getUri(Constants.Endpoints.USERS); + +const user = { + email: `${Math.log2(Math.random())}test@create.com`, + mobile: `${Math.round(Math.random())}792807794`, + password: '123456789', + profile: null, + role: ERoles.ADMIN, + username: `${Math.log2(Math.random())}TestCreate` +} as Body; -const ENDPOINT = `/api/${Constants.Endpoints.USERS}`; -let user: UserFixture = null; beforeAll(async () => { - user = await createUser(); + await createUser(); }); afterAll(async () => { @@ -16,107 +25,42 @@ afterAll(async () => { }); // NOTE test the fail, don't test the success -describe('CREATE USER', () => { +describe('#CREATE USER', () => { test('Fail if the user exist before', async () => { - const body = { - email: 'test@create1.com', - mobile: '0792807794', - password: '123456789', - username: 'testCreate1' - } as Body; const req1 = (await superAgent).post(ENDPOINT); - const res1 = await req1.send(body); - + await req1.send(user); const req2 = (await superAgent).post(ENDPOINT); - const res2 = await req2.send(body); - + const res2 = await req2.send(user); expect(res2.status).toBe(NetworkStatus.BAD_REQUEST); }); test('Special Char is not allowed', async () => { - const body = { - email: 'test@create2.com', - mobile: '0792807794', - password: '123456789', - username: 'testCreate2#$' - } as Body; const req = (await superAgent).post(ENDPOINT); - const res = await req.send(body); + const res = await req.send(Object.assign({}, user, { username: 'testCreate2#$' })); expect(res.status).toBe(NetworkStatus.BAD_REQUEST); }); - test('Mobile number shouldn"t be wrong', async () => { - const body = { - email: 'test@create3.com', - mobile: '079280779', - password: '123456789', - username: 'testCreate3' - } as Body; + test('Mobile number shouldnt be wrong', async () => { const req = (await superAgent).post(ENDPOINT); - const res = await req.send(body); + const res = await req.send(Object.assign({}, user, { mobile: '079280779' })); + expect(res.status).toBe(NetworkStatus.BAD_REQUEST); + }); + test('Fail if user role is not one of supported roles', async () => { + const req = (await superAgent).post(ENDPOINT); + const res = await req.send(Object.assign({}, user, { role: 100000 })); expect(res.status).toBe(NetworkStatus.BAD_REQUEST); }); -}); - -// STUB CREATE: user password should be hashed -// STUB READ: user body shouldn't have a password -// STUB CREATE: user should have an account -// STUB DELETE: remove associated account - -// describe('GET BY ${id}/', () => { -// test('Reject request without token', async () => { -// const res = await (await superAgent).get(`${ENDPOINT}/${user.id}`); -// expect(res.status).toBe(NetworkStatus.UNAUTHORIZED); -// }); - -// test('should fail if requested with id not of type ObjectId', async () => { -// // this will rise cast error -// const req = (await superAgent).get(`${ENDPOINT}/${undefined}`); -// const res = await req.set('Authorization', user.token); -// expect(res.status).toBe(NetworkStatus.BAD_REQUEST); -// }); - -// test('should fail if requested to non exist entity', async () => { -// const req = (await superAgent).get(`${ENDPOINT}/${new Types.ObjectId()}`); -// const res = await req.set('Authorization', user.token); -// expect(res.status).toBe(NetworkStatus.NOT_ACCEPTABLE); -// }); - -// test('resposne body should equal to', async () => { -// const req = (await superAgent).get(`${ENDPOINT}/${user.id}`); -// const res = await req.set('Authorization', user.token); -// const { data } = res.body; -// expect(data).toHaveProperty('username'); -// expect(data).toHaveProperty('email'); -// expect(data).toHaveProperty('mobile'); -// expect(data).toHaveProperty('createdAt'); -// expect(data).toHaveProperty('updatedAt'); -// expect(data).toHaveProperty('_id'); -// // password return, even it returned without being hashing -// expect(data).not.toHaveProperty('password'); -// }); -// }); - -// describe('DELETE BY ${id}/', () => { -// test('Reject request without token', async () => { -// const res = await (await superAgent).delete(`${ENDPOINT}/${user.id}`); -// expect(res.status).toBe(NetworkStatus.UNAUTHORIZED); -// }); - -// test('should fail if requested with id not of type ObjectId', async () => { -// const req = (await superAgent).delete(`${ENDPOINT}/${undefined}`); -// const res = await req.set('Authorization', user.token); -// expect(res.status).toBe(NetworkStatus.BAD_REQUEST); -// }); -// test('should fail if requested to non exist entity', async () => { -// const req = (await superAgent).delete(`${ENDPOINT}/${new Types.ObjectId()}`); -// const res = await req.set('Authorization', user.token); -// expect(res.status).toBe(NetworkStatus.NOT_ACCEPTABLE); -// }); + test.todo('user should have a defualt profile equal to empty {}'); + // test('Profile Should have an empty object when creating a new user', async () => { + // const req = (await superAgent).post(ENDPOINT); + // const res = await req.send(user); + // expect(((res.body) as Body).profile).toBe(undefined); + // // TODO: the profile object should has a default value in the service + // and not in model, then uncomment the below code + // // expect(((res.body) as Body).profile).toMatchObject({}); + // expect(res.status).toBe(NetworkStatus.CREATED); + // }); +}); -// test('resposne body should equal to', async () => { -// const req = (await superAgent).delete(`${ENDPOINT}/${user.id}`); -// const res = await req.set('Authorization', user.token); -// const { data } = res.body; -// expect(data).toBeNull(); -// }); -// }); +describe('#GET USER', () => { + test.todo(`user body shouldn't have a password`); +}); diff --git a/src/app/core/helpers/token.ts b/src/app/core/helpers/token.ts index 2698e5aa..a129bbc1 100644 --- a/src/app/core/helpers/token.ts +++ b/src/app/core/helpers/token.ts @@ -1,4 +1,10 @@ import jwt = require('jsonwebtoken'); +import { ERoles } from '@api/users'; + +export interface ITokenClaim { + role: ERoles; + id: string; +} class TokenService { @@ -21,7 +27,7 @@ class TokenService { * @param data token payload * @returns the encrypted token */ - public generateToken(data) { + public generateToken(data: ITokenClaim) { return jwt.sign(data, process.env.JWT_SECRET_KEY); } } diff --git a/src/app/core/utils/utils.service.ts b/src/app/core/utils/utils.service.ts index dd5671a7..4ec22f4a 100644 --- a/src/app/core/utils/utils.service.ts +++ b/src/app/core/utils/utils.service.ts @@ -79,6 +79,10 @@ export class AppUtils { return list.length > 0; } + public static assignObject(target, source1: T, source2?: Partial): T { + return Object.assign(target, source1, source2); + } + } // NOTE Utility class to be extended, so when you call build it will construct an instance from that class diff --git a/src/app/server.ts b/src/app/server.ts index 53349eb0..66af314a 100644 --- a/src/app/server.ts +++ b/src/app/server.ts @@ -20,7 +20,7 @@ export class NodeServer extends Application { public static async bootstrap() { // SECTION server init event log.debug('Start boostrapping server'); - envirnoment.load(); + envirnoment.load(StageLevel.DEV); const server = new NodeServer(); const httpServer = await server.populateServer(); // server.application.get('/socket/:name', handleSocket); diff --git a/src/app/shared/crud/crud.options.ts b/src/app/shared/crud/crud.options.ts index 7440ef30..ee5e849e 100644 --- a/src/app/shared/crud/crud.options.ts +++ b/src/app/shared/crud/crud.options.ts @@ -17,7 +17,5 @@ export interface ICrudOperation { } export interface ICrudOptions extends ICrudOperation { // TODO: Move it to each crud operation without the [attr] key - unique: Array<{ - attr: keyof Body, - }>; + unique: Array>; } diff --git a/src/app/shared/crud/crud.repo.ts b/src/app/shared/crud/crud.repo.ts index 9c3a2226..50e62cb6 100644 --- a/src/app/shared/crud/crud.repo.ts +++ b/src/app/shared/crud/crud.repo.ts @@ -1,4 +1,4 @@ -import { Model } from 'mongoose'; +import { Model, DeepPartial } from 'mongoose'; import { Document, Body } from '@lib/mongoose'; // TODO use repo and complete it export class Repo { @@ -18,4 +18,8 @@ export class Repo { public fetchById(id: string) { return this.model.findById(id); } + + public create(body: Body) { + return new this.model(body as any); + } } diff --git a/src/app/shared/crud/crud.router.ts b/src/app/shared/crud/crud.router.ts index bc7b0840..988fe373 100644 --- a/src/app/shared/crud/crud.router.ts +++ b/src/app/shared/crud/crud.router.ts @@ -12,7 +12,6 @@ export class CrudRouter { @Post('', Auth.isAuthenticated) public async create(req: Request, res: Response) { - console.log('This method is overrided'); const result = await this.service.create(req.body); if (result.exist) { throw new ErrorResponse(translate('entity_exist')); @@ -23,7 +22,7 @@ export class CrudRouter { @Put(':id', Auth.isAuthenticated) public async update(req: Request, res: Response) { - const entity = await this.service.update(req, res); + const entity = await this.service.update({ body: req.body, id: req.params.id }); if (!entity) { throw new ErrorResponse(translate('entity_not_found')); } diff --git a/src/app/shared/crud/crud.service.ts b/src/app/shared/crud/crud.service.ts index ac37dad8..8af67fff 100644 --- a/src/app/shared/crud/crud.service.ts +++ b/src/app/shared/crud/crud.service.ts @@ -1,9 +1,7 @@ import { ICrudOptions, ICrudHooks } from './crud.options'; -import { Repo } from '@shared/crud/crud.repo'; -import { Request, Response } from 'express'; import { Body, Document } from '@lib/mongoose'; -import { DeepPartial } from 'mongoose'; import { AppUtils } from '@core/utils'; +import { Repo } from './crud.repo'; function getHooks(options: Partial>) { return { @@ -14,21 +12,22 @@ function getHooks(options: Partial>) { export class CrudService { constructor( + // TODO: Should be protected not public public repo: Repo, private options: ICrudOptions = {} as any ) { } private async check(body) { if (this.options.unique) { - const opertaions = this.options.unique.map(async ({ attr }) => { - return !!(await this.repo.fetchOne({ [attr]: body[attr] } as any)); + const opertaions = this.options.unique.map(async (field) => { + return !!(await this.repo.fetchOne({ [field]: body[field] } as any)); }); return (await Promise.all(opertaions)).every((operation) => !!operation); } return false; } - public async create(body: Partial>) { + public async create(body: Body) { // TODO: customize the return object to clarify what's exactly the error const check = await this.check(body); @@ -39,7 +38,7 @@ export class CrudService { }; } - const entity = await new this.repo.model(body as DeepPartial>); + const entity = this.repo.create(body); const { pre, post } = getHooks(this.options.create); await pre(entity); @@ -65,13 +64,12 @@ export class CrudService { return entity; } - public async update(req: Request, res: Response) { - const { id } = req.params; - const entity = await this.repo.fetchById(id); + public async update(query: { body: Body, id: string }) { + const entity = await this.repo.fetchById(query.id); if (!entity) { return null; } - const check = await this.check(req.body); + const check = await this.check(query.body); if (check) { return null; @@ -79,7 +77,7 @@ export class CrudService { const { pre, post } = getHooks(this.options.update); await pre(entity); - entity.set(req.body); + entity.set(query.body); await entity.save(); await post(entity); return entity; diff --git a/src/app/shared/crud/crud.spec.ts b/src/app/shared/crud/crud.spec.ts index a7b302df..e2d4c022 100644 --- a/src/app/shared/crud/crud.spec.ts +++ b/src/app/shared/crud/crud.spec.ts @@ -5,6 +5,8 @@ import { Repo } from './crud.repo'; import { BaseModel } from '@lib/mongoose'; import { Schema } from 'mongoose'; import { Wrapper } from 'app/wrapper'; +import { superAgent } from '@test/index'; +import { NetworkStatus } from '@core/helpers'; interface ITest { first_name: string; @@ -43,3 +45,63 @@ describe('Add router to wrapper', () => { // REVIEW in create and update you should check and verify if the data was update or created successfully // other that the failur test // and in delete you must check that the entity no longer in database + +// describe('DELETE BY ${id}/', () => { +// test('Reject request without token', async () => { +// const res = await (await superAgent).delete(`${ENDPOINT}/${user.id}`); +// expect(res.status).toBe(NetworkStatus.UNAUTHORIZED); +// }); + +// test('should fail if requested with id not of type ObjectId', async () => { +// const req = (await superAgent).delete(`${ENDPOINT}/${undefined}`); +// const res = await req.set('Authorization', user.token); +// expect(res.status).toBe(NetworkStatus.BAD_REQUEST); +// }); + +// test('should fail if requested to non exist entity', async () => { +// const req = (await superAgent).delete(`${ENDPOINT}/${new Types.ObjectId()}`); +// const res = await req.set('Authorization', user.token); +// expect(res.status).toBe(NetworkStatus.NOT_ACCEPTABLE); +// }); + +// test('resposne body should equal to', async () => { +// const req = (await superAgent).delete(`${ENDPOINT}/${user.id}`); +// const res = await req.set('Authorization', user.token); +// const { data } = res.body; +// expect(data).toBeNull(); +// }); +// }); + +// describe('GET BY ${id}/', () => { +// test('Reject request without token', async () => { +// const res = await (await superAgent).get(`${ENDPOINT}/${user.id}`); +// expect(res.status).toBe(NetworkStatus.UNAUTHORIZED); +// }); + +// test('should fail if requested with id not of type ObjectId', async () => { +// // this will rise cast error +// const req = (await superAgent).get(`${ENDPOINT}/${undefined}`); +// const res = await req.set('Authorization', user.token); +// expect(res.status).toBe(NetworkStatus.BAD_REQUEST); +// }); + +// test('should fail if requested to non exist entity', async () => { +// const req = (await superAgent).get(`${ENDPOINT}/${new Types.ObjectId()}`); +// const res = await req.set('Authorization', user.token); +// expect(res.status).toBe(NetworkStatus.NOT_ACCEPTABLE); +// }); + +// test('resposne body should equal to', async () => { +// const req = (await superAgent).get(`${ENDPOINT}/${user.id}`); +// const res = await req.set('Authorization', user.token); +// const { data } = res.body; +// expect(data).toHaveProperty('username'); +// expect(data).toHaveProperty('email'); +// expect(data).toHaveProperty('mobile'); +// expect(data).toHaveProperty('createdAt'); +// expect(data).toHaveProperty('updatedAt'); +// expect(data).toHaveProperty('_id'); +// // password return, even it returned without being hashing +// expect(data).not.toHaveProperty('password'); +// }); +// }); diff --git a/src/lib/mongoose/field.ts b/src/lib/mongoose/field.ts index 06614688..bd7f964b 100644 --- a/src/lib/mongoose/field.ts +++ b/src/lib/mongoose/field.ts @@ -6,6 +6,7 @@ import { MongooseTypes } from '.'; export function Field(options: MongooseTypes.FieldOptions = {}) { return (instance: MongooseTypes.IFieldAttr & T, propertyKey: string) => { + // TODO: use reflect metadate instead of conditions if (instance && !instance.fields) { AppUtils.defineProperty(instance, 'fields', { value: {} }); } diff --git a/src/test/fixture.ts b/src/test/fixture.ts index 29d78026..ca4ff12a 100644 --- a/src/test/fixture.ts +++ b/src/test/fixture.ts @@ -20,12 +20,14 @@ export async function createUser() { username: `test`, mobile: '+962792807794' }); + console.log(res.body); user.id = res.body.data._id; user.token = sign({ id: user.id }, process.env.JWT_SECRET_KEY); return user; } export async function deleteUser() { + if (!user.id) { return; } const req = (await superAgent).delete(`${usersUri}/${user.id}`); const res = await req.set('Authorization', user.token); return res.body; diff --git a/tslint.json b/tslint.json index 3dc006e4..b435ef92 100644 --- a/tslint.json +++ b/tslint.json @@ -36,7 +36,7 @@ "no-console": false, "no-string-literal": false, "no-empty": [ - true, + false, "allow-empty-catch" ], "no-floating-promises": false,