mirror of
https://github.com/hantsy/nestjs-rest-sample.git
synced 2025-12-08 20:36:27 +00:00
test(e2e): add endpoints testing
This commit is contained in:
parent
202f2b0e8e
commit
450537dab7
46
.github/workflows/e2e.yml
vendored
Normal file
46
.github/workflows/e2e.yml
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
name: e2e
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths-ignore:
|
||||||
|
- "docs/**"
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- release/*
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- synchronize
|
||||||
|
- reopened
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Setup NodeJS
|
||||||
|
uses: actions/setup-node@v2-beta
|
||||||
|
with:
|
||||||
|
node-version: "14"
|
||||||
|
|
||||||
|
- name: Cache Node.js modules
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
# npm cache files are stored in `~/.npm` on Linux/macOS
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.OS }}-node-
|
||||||
|
${{ runner.OS }}-
|
||||||
|
|
||||||
|
- name: Running mongodb service
|
||||||
|
run: |
|
||||||
|
docker-compose up -d mongodb
|
||||||
|
docker ps -a
|
||||||
|
|
||||||
|
# install dependencies and build the project
|
||||||
|
- name: Running e2e testing
|
||||||
|
run: |
|
||||||
|
npm install
|
||||||
|
npm run test:e2e -- --runInBand --forceExit
|
||||||
61
package-lock.json
generated
61
package-lock.json
generated
@ -1189,10 +1189,10 @@
|
|||||||
"integrity": "sha512-wo2rHprtDzTHf4tiSxavktJ52ntiwmg7eHNGFLH38G1of8OfGVwOc1sVbpM4jN/HK/rCMhYOi6xzoPqsv0537A==",
|
"integrity": "sha512-wo2rHprtDzTHf4tiSxavktJ52ntiwmg7eHNGFLH38G1of8OfGVwOc1sVbpM4jN/HK/rCMhYOi6xzoPqsv0537A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@golevelup/nestjs-testing": {
|
"@golevelup/ts-jest": {
|
||||||
"version": "0.1.2",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npm.taobao.org/@golevelup/nestjs-testing/download/@golevelup/nestjs-testing-0.1.2.tgz",
|
"resolved": "https://registry.npm.taobao.org/@golevelup/ts-jest/download/@golevelup/ts-jest-0.3.0.tgz",
|
||||||
"integrity": "sha1-MU6H5jTVgXl9CGyfX0Xc6AKtkZo=",
|
"integrity": "sha1-coMoYwTPl7FzYOeOOtHWbnGcQy8=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@istanbuljs/load-nyc-config": {
|
"@istanbuljs/load-nyc-config": {
|
||||||
@ -2208,6 +2208,13 @@
|
|||||||
"lodash.get": "4.4.2",
|
"lodash.get": "4.4.2",
|
||||||
"lodash.set": "4.3.2",
|
"lodash.set": "4.3.2",
|
||||||
"uuid": "8.1.0"
|
"uuid": "8.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"uuid": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg=="
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@nestjs/core": {
|
"@nestjs/core": {
|
||||||
@ -2768,6 +2775,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/validator": {
|
||||||
|
"version": "13.0.0",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/@types/validator/download/@types/validator-13.0.0.tgz",
|
||||||
|
"integrity": "sha1-Nl8b+Taurd0IVvxBqh1vgtiO5bM="
|
||||||
|
},
|
||||||
"@types/webpack": {
|
"@types/webpack": {
|
||||||
"version": "4.41.17",
|
"version": "4.41.17",
|
||||||
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.17.tgz",
|
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.17.tgz",
|
||||||
@ -4226,6 +4238,11 @@
|
|||||||
"safe-buffer": "^5.0.1"
|
"safe-buffer": "^5.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"class-transformer": {
|
||||||
|
"version": "0.2.3",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/class-transformer/download/class-transformer-0.2.3.tgz",
|
||||||
|
"integrity": "sha1-WYySynHcynP5HMuHXXSjhHzPoy0="
|
||||||
|
},
|
||||||
"class-utils": {
|
"class-utils": {
|
||||||
"version": "0.3.6",
|
"version": "0.3.6",
|
||||||
"resolved": "https://registry.npm.taobao.org/class-utils/download/class-utils-0.3.6.tgz",
|
"resolved": "https://registry.npm.taobao.org/class-utils/download/class-utils-0.3.6.tgz",
|
||||||
@ -4249,6 +4266,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"class-validator": {
|
||||||
|
"version": "0.12.2",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/class-validator/download/class-validator-0.12.2.tgz",
|
||||||
|
"integrity": "sha1-LOty+Ihz6ccUz1+cJ4y8cfb2yO8=",
|
||||||
|
"requires": {
|
||||||
|
"@types/validator": "13.0.0",
|
||||||
|
"google-libphonenumber": "^3.2.8",
|
||||||
|
"tslib": ">=1.9.0",
|
||||||
|
"validator": "13.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"cli-color": {
|
"cli-color": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.0.tgz",
|
||||||
@ -6954,6 +6982,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"google-libphonenumber": {
|
||||||
|
"version": "3.2.10",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/google-libphonenumber/download/google-libphonenumber-3.2.10.tgz",
|
||||||
|
"integrity": "sha1-AhoxRlJ0fXNqOeLmDcZw8EMUJa0="
|
||||||
|
},
|
||||||
"graceful-fs": {
|
"graceful-fs": {
|
||||||
"version": "4.2.4",
|
"version": "4.2.4",
|
||||||
"resolved": "https://registry.npm.taobao.org/graceful-fs/download/graceful-fs-4.2.4.tgz",
|
"resolved": "https://registry.npm.taobao.org/graceful-fs/download/graceful-fs-4.2.4.tgz",
|
||||||
@ -9231,6 +9264,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"jest-mock-extended": {
|
||||||
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/jest-mock-extended/download/jest-mock-extended-1.0.9.tgz",
|
||||||
|
"integrity": "sha1-NIk4eOLOi4Yjzx8Wq+KGHeH+JbQ=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"ts-essentials": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"jest-pnp-resolver": {
|
"jest-pnp-resolver": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz",
|
||||||
@ -13904,6 +13946,12 @@
|
|||||||
"integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=",
|
"integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"ts-essentials": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/ts-essentials/download/ts-essentials-4.0.0.tgz",
|
||||||
|
"integrity": "sha1-UGxCsnC70EZVdLkEFlMxdbCSBas=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"ts-jest": {
|
"ts-jest": {
|
||||||
"version": "26.1.1",
|
"version": "26.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.1.1.tgz",
|
||||||
@ -14313,6 +14361,11 @@
|
|||||||
"spdx-expression-parse": "^3.0.0"
|
"spdx-expression-parse": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"validator": {
|
||||||
|
"version": "13.0.0",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/validator/download/validator-13.0.0.tgz",
|
||||||
|
"integrity": "sha1-D7bGu1IY6iPTaKg0fm0PWnDjvKs="
|
||||||
|
},
|
||||||
"vary": {
|
"vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
|||||||
13
package.json
13
package.json
@ -21,12 +21,14 @@
|
|||||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^7.2.0",
|
"@nestjs/common": "^7.0.0",
|
||||||
"@nestjs/config": "^0.5.0",
|
"@nestjs/config": "^0.5.0",
|
||||||
"@nestjs/core": "^7.2.0",
|
"@nestjs/core": "^7.0.0",
|
||||||
"@nestjs/jwt": "^7.0.0",
|
"@nestjs/jwt": "^7.0.0",
|
||||||
"@nestjs/passport": "^7.1.0",
|
"@nestjs/passport": "^7.1.0",
|
||||||
"@nestjs/platform-express": "^7.2.0",
|
"@nestjs/platform-express": "^7.0.0",
|
||||||
|
"class-transformer": "^0.2.3",
|
||||||
|
"class-validator": "^0.12.2",
|
||||||
"mongoose": "^5.9.20",
|
"mongoose": "^5.9.20",
|
||||||
"passport": "^0.4.1",
|
"passport": "^0.4.1",
|
||||||
"passport-jwt": "^4.0.0",
|
"passport-jwt": "^4.0.0",
|
||||||
@ -38,10 +40,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^9.0.1",
|
"@commitlint/cli": "^9.0.1",
|
||||||
"@commitlint/config-conventional": "^9.0.1",
|
"@commitlint/config-conventional": "^9.0.1",
|
||||||
"@golevelup/nestjs-testing": "^0.1.2",
|
"@golevelup/ts-jest": "^0.3.0",
|
||||||
"@nestjs/cli": "^7.4.1",
|
"@nestjs/cli": "^7.4.1",
|
||||||
"@nestjs/schematics": "^7.0.0",
|
"@nestjs/schematics": "^7.0.0",
|
||||||
"@nestjs/testing": "^7.2.0",
|
"@nestjs/testing": "^7.0.0",
|
||||||
"@types/express": "^4.17.3",
|
"@types/express": "^4.17.3",
|
||||||
"@types/jest": "26.0.3",
|
"@types/jest": "26.0.3",
|
||||||
"@types/mongoose": "^5.7.29",
|
"@types/mongoose": "^5.7.29",
|
||||||
@ -56,6 +58,7 @@
|
|||||||
"eslint-plugin-import": "^2.22.0",
|
"eslint-plugin-import": "^2.22.0",
|
||||||
"husky": "^4.2.5",
|
"husky": "^4.2.5",
|
||||||
"jest": "26.1.0",
|
"jest": "26.1.0",
|
||||||
|
"jest-mock-extended": "^1.0.9",
|
||||||
"prettier": "^2.0.5",
|
"prettier": "^2.0.5",
|
||||||
"supertest": "^4.0.2",
|
"supertest": "^4.0.2",
|
||||||
"ts-jest": "26.1.1",
|
"ts-jest": "26.1.1",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { createMock } from '@golevelup/nestjs-testing';
|
import { createMock } from '@golevelup/ts-jest';
|
||||||
import { ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
import { ExecutionContext } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { JwtAuthGuard } from './jwt-auth.guard';
|
import { JwtAuthGuard } from './jwt-auth.guard';
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ describe('LocalAuthGuard', () => {
|
|||||||
try {
|
try {
|
||||||
guard.handleRequest(undefined, undefined, undefined);
|
guard.handleRequest(undefined, undefined, undefined);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// console.log(e);
|
// console.log(e);
|
||||||
expect(e).toBeDefined();
|
expect(e).toBeDefined();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,12 +1,24 @@
|
|||||||
import { RolesGuard } from './roles.guard';
|
import { createMock } from '@golevelup/ts-jest';
|
||||||
import { Reflector } from '@nestjs/core';
|
|
||||||
import { TestingModule, Test } from '@nestjs/testing';
|
|
||||||
import { ExecutionContext } from '@nestjs/common';
|
import { ExecutionContext } from '@nestjs/common';
|
||||||
import { createMock } from '@golevelup/nestjs-testing';
|
import { HttpArgumentsHost } from '@nestjs/common/interfaces';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import {
|
||||||
|
anyFunction,
|
||||||
|
anyString,
|
||||||
|
instance,
|
||||||
|
mock,
|
||||||
|
verify,
|
||||||
|
when,
|
||||||
|
reset,
|
||||||
|
} from 'ts-mockito';
|
||||||
|
import { mock as jestMock, any, mockClear, mockDeep } from 'jest-mock-extended';
|
||||||
import { RoleType } from '../database/role-type.enum';
|
import { RoleType } from '../database/role-type.enum';
|
||||||
|
import { HAS_ROLES_KEY } from './auth.constants';
|
||||||
import { AuthenticatedRequest } from './authenticated-request.interface';
|
import { AuthenticatedRequest } from './authenticated-request.interface';
|
||||||
|
import { RolesGuard } from './roles.guard';
|
||||||
|
|
||||||
describe('RolesGuard', () => {
|
xdescribe('RolesGuard', () => {
|
||||||
let guard: RolesGuard;
|
let guard: RolesGuard;
|
||||||
let reflector: Reflector;
|
let reflector: Reflector;
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@ -27,59 +39,208 @@ describe('RolesGuard', () => {
|
|||||||
reflector = module.get<Reflector>(Reflector);
|
reflector = module.get<Reflector>(Reflector);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
expect(guard).toBeDefined();
|
expect(guard).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip(return true) if the `HasRoles` decorator is not set', async () => {
|
it('should skip(return true) if the `HasRoles` decorator is not set', async () => {
|
||||||
jest.spyOn(reflector, 'get').mockImplementation((a: any, b: any) => {
|
jest.spyOn(reflector, 'get').mockImplementation((a: any, b: any) => []);
|
||||||
return undefined;
|
const context = createMock<ExecutionContext>();
|
||||||
});
|
const result = await guard.canActivate(context);
|
||||||
expect(
|
|
||||||
guard.canActivate(
|
expect(result).toBeTruthy();
|
||||||
createMock<ExecutionContext>({
|
|
||||||
getHandler: jest.fn(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
expect(reflector.get).toBeCalled();
|
expect(reflector.get).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true if the `HasRoles` decorator is set', async () => {
|
it('should return true if the `HasRoles` decorator is set', async () => {
|
||||||
jest.spyOn(reflector, 'get').mockImplementation((a: any, b: any) => {
|
jest
|
||||||
return [RoleType.USER];
|
.spyOn(reflector, 'get')
|
||||||
|
.mockImplementation((a: any, b: any) => [RoleType.USER]);
|
||||||
|
const context = createMock<ExecutionContext>({
|
||||||
|
getHandler: jest.fn(),
|
||||||
|
switchToHttp: jest.fn().mockReturnValue({
|
||||||
|
getRequest: jest.fn().mockReturnValue({
|
||||||
|
user: { roles: [RoleType.USER] },
|
||||||
|
} as AuthenticatedRequest),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
expect(
|
|
||||||
guard.canActivate(
|
const result = await guard.canActivate(context);
|
||||||
createMock<ExecutionContext>({
|
expect(result).toBeTruthy();
|
||||||
getHandler: jest.fn(),
|
|
||||||
switchToHttp: jest.fn().mockReturnValue({
|
|
||||||
getRequest: jest.fn().mockReturnValue({
|
|
||||||
user: { roles: [RoleType.USER]},
|
|
||||||
} as AuthenticatedRequest),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
expect(reflector.get).toBeCalled();
|
expect(reflector.get).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if the `HasRoles` decorator is set but role is not allowed', async () => {
|
it('should return false if the `HasRoles` decorator is set but role is not allowed', async () => {
|
||||||
jest.spyOn(reflector, 'get').mockImplementation((a: any, b: any) => {
|
jest.spyOn(reflector, 'get').mockReturnValue([RoleType.ADMIN]);
|
||||||
return [RoleType.ADMIN];
|
const request = {
|
||||||
|
user: { roles: [RoleType.USER] },
|
||||||
|
} as AuthenticatedRequest;
|
||||||
|
const context = createMock<ExecutionContext>();
|
||||||
|
const httpArgsHost = createMock<HttpArgumentsHost>({
|
||||||
|
getRequest: () => request,
|
||||||
});
|
});
|
||||||
expect(
|
context.switchToHttp.mockImplementation(() => httpArgsHost);
|
||||||
guard.canActivate(
|
|
||||||
createMock<ExecutionContext>({
|
const result = await guard.canActivate(context);
|
||||||
getHandler: jest.fn(),
|
expect(result).toBeFalsy();
|
||||||
switchToHttp: jest.fn().mockReturnValue({
|
|
||||||
getRequest: jest.fn().mockReturnValue({
|
|
||||||
user: { roles: [RoleType.USER]},
|
|
||||||
} as AuthenticatedRequest),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
).toBe(false);
|
|
||||||
expect(reflector.get).toBeCalled();
|
expect(reflector.get).toBeCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('RolesGuard(ts-mockito)', () => {
|
||||||
|
let guard: RolesGuard;
|
||||||
|
const reflecter = mock(Reflector);
|
||||||
|
beforeEach(() => {
|
||||||
|
guard = new RolesGuard(instance(reflecter));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip(return true) if the `HasRoles` decorator is not set', async () => {
|
||||||
|
const context = mock<ExecutionContext>();
|
||||||
|
when(context.getHandler()).thenReturn({} as any);
|
||||||
|
|
||||||
|
const contextInstacne = instance(context);
|
||||||
|
when(
|
||||||
|
reflecter.get<RoleType[]>(HAS_ROLES_KEY, contextInstacne.getHandler()),
|
||||||
|
).thenReturn([] as RoleType[]);
|
||||||
|
const result = await guard.canActivate(contextInstacne);
|
||||||
|
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
verify(
|
||||||
|
reflecter.get<RoleType[]>(HAS_ROLES_KEY, contextInstacne.getHandler()),
|
||||||
|
).once();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if the `HasRoles` decorator is set', async () => {
|
||||||
|
const context = mock<ExecutionContext>();
|
||||||
|
|
||||||
|
when(context.getHandler()).thenReturn({} as any);
|
||||||
|
|
||||||
|
const arguHost = mock<HttpArgumentsHost>();
|
||||||
|
when(arguHost.getRequest()).thenReturn({
|
||||||
|
user: { roles: [RoleType.USER] },
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
when(context.switchToHttp()).thenReturn(instance(arguHost));
|
||||||
|
const contextInstacne = instance(context);
|
||||||
|
|
||||||
|
when(
|
||||||
|
reflecter.get<RoleType[]>(HAS_ROLES_KEY, contextInstacne.getHandler()),
|
||||||
|
).thenReturn([RoleType.USER] as RoleType[]);
|
||||||
|
|
||||||
|
const result = await guard.canActivate(contextInstacne);
|
||||||
|
console.log(result);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
verify(
|
||||||
|
reflecter.get<RoleType[]>(HAS_ROLES_KEY, contextInstacne.getHandler()),
|
||||||
|
).once();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if the `HasRoles` decorator is set but role is not allowed', async () => {
|
||||||
|
const context = mock<ExecutionContext>();
|
||||||
|
|
||||||
|
when(context.getHandler()).thenReturn({} as any);
|
||||||
|
|
||||||
|
// logged in as USER
|
||||||
|
const arguHost = mock<HttpArgumentsHost>();
|
||||||
|
when(arguHost.getRequest()).thenReturn({
|
||||||
|
user: { roles: [RoleType.USER] },
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
when(context.switchToHttp()).thenReturn(instance(arguHost));
|
||||||
|
const contextInstacne = instance(context);
|
||||||
|
|
||||||
|
// but requires ADMIN
|
||||||
|
when(
|
||||||
|
reflecter.get<RoleType[]>(HAS_ROLES_KEY, contextInstacne.getHandler()),
|
||||||
|
).thenReturn([RoleType.ADMIN] as RoleType[]);
|
||||||
|
|
||||||
|
const result = await guard.canActivate(contextInstacne);
|
||||||
|
console.log(result);
|
||||||
|
expect(result).toBeFalsy();
|
||||||
|
verify(
|
||||||
|
reflecter.get<RoleType[]>(HAS_ROLES_KEY, contextInstacne.getHandler()),
|
||||||
|
).once();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('RoelsGuard(jest-mock-extended)', () => {
|
||||||
|
let guard: RolesGuard;
|
||||||
|
const reflecter = jestMock<Reflector>();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
guard = new RolesGuard(reflecter);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mockClear(reflecter);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(guard).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip(return true) if the `HasRoles` decorator is not set', async () => {
|
||||||
|
const context = jestMock<ExecutionContext>();
|
||||||
|
context.getHandler.mockReturnValue({} as any);
|
||||||
|
reflecter.get
|
||||||
|
.mockReturnValue([])
|
||||||
|
.calledWith(HAS_ROLES_KEY, context.getHandler());
|
||||||
|
|
||||||
|
const result = await guard.canActivate(context);
|
||||||
|
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
expect(reflecter.get).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if the `HasRoles` decorator is set', async () => {
|
||||||
|
const context = jestMock<ExecutionContext>();
|
||||||
|
context.getHandler.mockReturnValue({} as any);
|
||||||
|
|
||||||
|
const arguHost = jestMock<HttpArgumentsHost>();
|
||||||
|
arguHost.getRequest.mockReturnValue({
|
||||||
|
user: { roles: [RoleType.USER] },
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
context.switchToHttp.mockReturnValue(arguHost);
|
||||||
|
|
||||||
|
reflecter.get
|
||||||
|
.mockReturnValue([RoleType.USER])
|
||||||
|
.calledWith(HAS_ROLES_KEY, context.getHandler());
|
||||||
|
|
||||||
|
const result = await guard.canActivate(context);
|
||||||
|
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
expect(reflecter.get).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if the `HasRoles` decorator is set but role is not allowed', async () => {
|
||||||
|
// logged in as USER
|
||||||
|
const context = jestMock<ExecutionContext>();
|
||||||
|
context.getHandler.mockReturnValue({} as any);
|
||||||
|
|
||||||
|
const arguHost = jestMock<HttpArgumentsHost>();
|
||||||
|
arguHost.getRequest.mockReturnValue({
|
||||||
|
user: { roles: [RoleType.USER] },
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
context.switchToHttp.mockReturnValue(arguHost);
|
||||||
|
|
||||||
|
//but requires ADMIN
|
||||||
|
reflecter.get
|
||||||
|
.mockReturnValue([RoleType.ADMIN])
|
||||||
|
.calledWith(HAS_ROLES_KEY, context.getHandler());
|
||||||
|
|
||||||
|
const result = await guard.canActivate(context);
|
||||||
|
|
||||||
|
expect(result).toBeFalsy();
|
||||||
|
expect(reflecter.get).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
import { RoleType } from '../database/role-type.enum';
|
import { RoleType } from '../database/role-type.enum';
|
||||||
import { HAS_ROLES_KEY } from './auth.constants';
|
import { HAS_ROLES_KEY } from './auth.constants';
|
||||||
import { AuthenticatedRequest } from './authenticated-request.interface';
|
import { AuthenticatedRequest } from './authenticated-request.interface';
|
||||||
@ -15,11 +15,13 @@ export class RolesGuard implements CanActivate {
|
|||||||
HAS_ROLES_KEY,
|
HAS_ROLES_KEY,
|
||||||
context.getHandler(),
|
context.getHandler(),
|
||||||
);
|
);
|
||||||
if (!roles) {
|
if (!roles || roles.length == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { user }= context.switchToHttp().getRequest() as AuthenticatedRequest;
|
const {
|
||||||
return user.roles && user.roles.some(r => roles.includes(r));
|
user,
|
||||||
|
} = context.switchToHttp().getRequest() as AuthenticatedRequest;
|
||||||
|
return user.roles && user.roles.some((r) => roles.includes(r));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,8 @@ export const databaseProviders = [
|
|||||||
connect(dbConfig.uri, {
|
connect(dbConfig.uri, {
|
||||||
useNewUrlParser: true,
|
useNewUrlParser: true,
|
||||||
useUnifiedTopology: true,
|
useUnifiedTopology: true,
|
||||||
|
//see: https://mongoosejs.com/docs/deprecations.html#findandmodify
|
||||||
|
useFindAndModify: false
|
||||||
}),
|
}),
|
||||||
inject: [mongodbConfig.KEY],
|
inject: [mongodbConfig.KEY],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
@ -7,6 +8,7 @@ async function bootstrap() {
|
|||||||
// enable shutdown hooks explicitly.
|
// enable shutdown hooks explicitly.
|
||||||
app.enableShutdownHooks();
|
app.enableShutdownHooks();
|
||||||
|
|
||||||
|
app.useGlobalPipes(new ValidationPipe());
|
||||||
app.enableCors();
|
app.enableCors();
|
||||||
//app.useLogger();
|
//app.useLogger();
|
||||||
await app.listen(3000);
|
await app.listen(3000);
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
import { IsNotEmpty } from 'class-validator';
|
||||||
export class CreateCommentDto {
|
export class CreateCommentDto {
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
readonly content: string;
|
readonly content: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,9 @@
|
|||||||
|
import { IsNotEmpty } from 'class-validator';
|
||||||
export class CreatePostDto {
|
export class CreatePostDto {
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
readonly title: string;
|
readonly title: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
readonly content: string;
|
readonly content: string;
|
||||||
}
|
}
|
||||||
|
|||||||
28
src/post/parse-object-id.pipe.spec.ts
Normal file
28
src/post/parse-object-id.pipe.spec.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import * as mongoose from 'mongoose';
|
||||||
|
import { ParseObjectIdPipe } from './parse-object-id.pipe';
|
||||||
|
|
||||||
|
describe('ParseObjectIdPipe', () => {
|
||||||
|
let isObjectId;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
isObjectId = new ParseObjectIdPipe();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(isObjectId).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if valid', () => {
|
||||||
|
const validId = new mongoose.Types.ObjectId().toHexString();
|
||||||
|
const result = isObjectId.transform(validId, {} as any);
|
||||||
|
expect(result).toEqual(validId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if invalid', () => {
|
||||||
|
try {
|
||||||
|
const result = isObjectId.transform('anerror', {} as any);
|
||||||
|
} catch (e) {
|
||||||
|
expect(e).not.toBeNull();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
17
src/post/parse-object-id.pipe.ts
Normal file
17
src/post/parse-object-id.pipe.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import {
|
||||||
|
ArgumentMetadata,
|
||||||
|
BadRequestException,
|
||||||
|
Injectable,
|
||||||
|
PipeTransform,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import * as mongoose from 'mongoose';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ParseObjectIdPipe implements PipeTransform<string, string> {
|
||||||
|
transform(value: string, metadata: ArgumentMetadata) {
|
||||||
|
if (!mongoose.isValidObjectId(value)) {
|
||||||
|
throw new BadRequestException(`$value is not a valid mongoose object id`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@ import {
|
|||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
OnModuleDestroy,
|
OnModuleDestroy,
|
||||||
OnModuleInit
|
OnModuleInit,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Model } from 'mongoose';
|
import { Model } from 'mongoose';
|
||||||
import { Comment } from '../database/comment.model';
|
import { Comment } from '../database/comment.model';
|
||||||
@ -32,33 +32,21 @@ export class PostDataInitializerService
|
|||||||
@Inject(POST_MODEL) private postModel: Model<Post>,
|
@Inject(POST_MODEL) private postModel: Model<Post>,
|
||||||
@Inject(COMMENT_MODEL) private commentModel: Model<Comment>,
|
@Inject(COMMENT_MODEL) private commentModel: Model<Comment>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
onModuleInit(): void {
|
onModuleInit(): void {
|
||||||
console.log('(PostModule) is initialized...');
|
console.log('(PostModule) is initialized...');
|
||||||
this.data.forEach(d => {
|
this.postModel.insertMany(this.data).then((r) => console.log(r));
|
||||||
this.postModel.create(d).then(saved => console.log(saved));
|
// Promise.all(this.data.map((d) => this.postModel.create(d))).then((saved) =>
|
||||||
});
|
// console.log(saved),
|
||||||
|
// );
|
||||||
this.postModel
|
|
||||||
.create({
|
|
||||||
title: 'Model relations in Mongoose',
|
|
||||||
content: 'content of Model relations in Mongoose',
|
|
||||||
})
|
|
||||||
.then(post =>
|
|
||||||
this.commentModel.create({
|
|
||||||
post: { _id: post._id },
|
|
||||||
content: 'comment of Model relations in Mongoose',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.then(saved => console.log(saved));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onModuleDestroy(): void {
|
onModuleDestroy(): void {
|
||||||
console.log('(PostModule) is being destroyed...');
|
Promise.all([
|
||||||
this.postModel
|
this.postModel.deleteMany({}),
|
||||||
.deleteMany({})
|
this.commentModel.deleteMany({}),
|
||||||
.then(del => console.log(`deleted ${del.deletedCount} rows of posts`));
|
]).then((data) => {
|
||||||
this.commentModel
|
console.log(data);
|
||||||
.deleteMany({})
|
});
|
||||||
.then(del => console.log(`deleted ${del.deletedCount} rows of comments`));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { PostController } from './post.controller';
|
|||||||
import { PostService } from './post.service';
|
import { PostService } from './post.service';
|
||||||
import { PostServiceStub } from './post.service.stub';
|
import { PostServiceStub } from './post.service.stub';
|
||||||
import { UpdatePostDto } from './update-post.dto';
|
import { UpdatePostDto } from './update-post.dto';
|
||||||
import { createMock } from '@golevelup/nestjs-testing';
|
import { createMock } from '@golevelup/ts-jest';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
|
|
||||||
describe('Post Controller', () => {
|
describe('Post Controller', () => {
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import { CreatePostDto } from './create-post.dto';
|
|||||||
import { PostService } from './post.service';
|
import { PostService } from './post.service';
|
||||||
import { UpdatePostDto } from './update-post.dto';
|
import { UpdatePostDto } from './update-post.dto';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
import { ParseObjectIdPipe } from './parse-object-id.pipe';
|
||||||
|
|
||||||
@Controller({ path: 'posts', scope: Scope.REQUEST })
|
@Controller({ path: 'posts', scope: Scope.REQUEST })
|
||||||
export class PostController {
|
export class PostController {
|
||||||
@ -41,7 +42,7 @@ export class PostController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
getPostById(@Param('id') id: string): Observable<BlogPost> {
|
getPostById(@Param('id', ParseObjectIdPipe) id: string): Observable<BlogPost> {
|
||||||
return this.postService.findById(id);
|
return this.postService.findById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +67,7 @@ export class PostController {
|
|||||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
@HasRoles(RoleType.USER, RoleType.ADMIN)
|
@HasRoles(RoleType.USER, RoleType.ADMIN)
|
||||||
updatePost(
|
updatePost(
|
||||||
@Param('id') id: string,
|
@Param('id', ParseObjectIdPipe) id: string,
|
||||||
@Body() post: UpdatePostDto,
|
@Body() post: UpdatePostDto,
|
||||||
@Res() res: Response,
|
@Res() res: Response,
|
||||||
): Observable<Response> {
|
): Observable<Response> {
|
||||||
@ -81,7 +82,7 @@ export class PostController {
|
|||||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
@HasRoles(RoleType.ADMIN)
|
@HasRoles(RoleType.ADMIN)
|
||||||
deletePostById(
|
deletePostById(
|
||||||
@Param('id') id: string,
|
@Param('id', ParseObjectIdPipe) id: string,
|
||||||
@Res() res: Response,
|
@Res() res: Response,
|
||||||
): Observable<Response> {
|
): Observable<Response> {
|
||||||
return this.postService.deleteById(id).pipe(
|
return this.postService.deleteById(id).pipe(
|
||||||
@ -95,7 +96,7 @@ export class PostController {
|
|||||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
@HasRoles(RoleType.USER)
|
@HasRoles(RoleType.USER)
|
||||||
createCommentForPost(
|
createCommentForPost(
|
||||||
@Param('id') id: string,
|
@Param('id', ParseObjectIdPipe) id: string,
|
||||||
@Body() data: CreateCommentDto,
|
@Body() data: CreateCommentDto,
|
||||||
@Res() res: Response,
|
@Res() res: Response,
|
||||||
): Observable<Response> {
|
): Observable<Response> {
|
||||||
@ -110,7 +111,7 @@ export class PostController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/comments')
|
@Get(':id/comments')
|
||||||
getAllCommentsOfPost(@Param('id') id: string): Observable<Comment[]> {
|
getAllCommentsOfPost(@Param('id', ParseObjectIdPipe) id: string): Observable<Comment[]> {
|
||||||
return this.postService.commentsOf(id);
|
return this.postService.commentsOf(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { FilterQuery, Model } from 'mongoose';
|
import { FilterQuery, Model } from 'mongoose';
|
||||||
|
import { Comment } from '../database/comment.model';
|
||||||
import { COMMENT_MODEL, POST_MODEL } from '../database/database.constants';
|
import { COMMENT_MODEL, POST_MODEL } from '../database/database.constants';
|
||||||
import { Post } from '../database/post.model';
|
import { Post } from '../database/post.model';
|
||||||
import { Comment } from '../database/comment.model';
|
|
||||||
import { PostService } from './post.service';
|
import { PostService } from './post.service';
|
||||||
|
|
||||||
describe('PostService', () => {
|
describe('PostService', () => {
|
||||||
@ -28,6 +28,7 @@ describe('PostService', () => {
|
|||||||
exec: jest.fn(),
|
exec: jest.fn(),
|
||||||
deleteMany: jest.fn(),
|
deleteMany: jest.fn(),
|
||||||
deleteOne: jest.fn(),
|
deleteOne: jest.fn(),
|
||||||
|
updateOne: jest.fn(),
|
||||||
findOneAndUpdate: jest.fn(),
|
findOneAndUpdate: jest.fn(),
|
||||||
findOneAndDelete: jest.fn(),
|
findOneAndDelete: jest.fn(),
|
||||||
},
|
},
|
||||||
@ -39,6 +40,8 @@ describe('PostService', () => {
|
|||||||
constructor: jest.fn(),
|
constructor: jest.fn(),
|
||||||
find: jest.fn(),
|
find: jest.fn(),
|
||||||
findOne: jest.fn(),
|
findOne: jest.fn(),
|
||||||
|
updateOne: jest.fn(),
|
||||||
|
deleteOne: jest.fn(),
|
||||||
update: jest.fn(),
|
update: jest.fn(),
|
||||||
create: jest.fn(),
|
create: jest.fn(),
|
||||||
remove: jest.fn(),
|
remove: jest.fn(),
|
||||||
@ -119,24 +122,42 @@ describe('PostService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('findById with an existing id should return one post', (done) => {
|
describe('findByid', () => {
|
||||||
const found = {
|
it('if exists return one post', (done) => {
|
||||||
_id: '5ee49c3115a4e75254bb732e',
|
const found = {
|
||||||
title: 'Generate a NestJS project',
|
_id: '5ee49c3115a4e75254bb732e',
|
||||||
content: 'content',
|
title: 'Generate a NestJS project',
|
||||||
};
|
content: 'content',
|
||||||
|
};
|
||||||
|
|
||||||
jest.spyOn(model, 'findOne').mockReturnValue({
|
jest.spyOn(model, 'findOne').mockReturnValue({
|
||||||
exec: jest.fn().mockResolvedValueOnce(found) as any,
|
exec: jest.fn().mockResolvedValueOnce(found) as any,
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
service.findById('1').subscribe({
|
service.findById('1').subscribe({
|
||||||
next: (data) => {
|
next: (data) => {
|
||||||
expect(data._id).toBe('5ee49c3115a4e75254bb732e');
|
expect(data._id).toBe('5ee49c3115a4e75254bb732e');
|
||||||
expect(data.title).toEqual('Generate a NestJS project');
|
expect(data.title).toEqual('Generate a NestJS project');
|
||||||
},
|
},
|
||||||
error: (error) => console.log(error),
|
error: (error) => console.log(error),
|
||||||
complete: done(),
|
complete: done(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if not found throw an NotFoundException', (done) => {
|
||||||
|
jest.spyOn(model, 'findOne').mockReturnValue({
|
||||||
|
exec: jest.fn().mockResolvedValueOnce(null) as any,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
service.findById('1').subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
console.log(data);
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
},
|
||||||
|
complete: done(),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -164,40 +185,80 @@ describe('PostService', () => {
|
|||||||
expect(model.create).toBeCalledTimes(1);
|
expect(model.create).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update post', (done) => {
|
describe('update', () => {
|
||||||
const toUpdated = {
|
it('perform update if post exists', (done) => {
|
||||||
_id: '5ee49c3115a4e75254bb732e',
|
const toUpdated = {
|
||||||
title: 'test title',
|
_id: '5ee49c3115a4e75254bb732e',
|
||||||
content: 'test content',
|
title: 'test title',
|
||||||
};
|
content: 'test content',
|
||||||
|
};
|
||||||
|
|
||||||
jest.spyOn(model, 'findOneAndUpdate').mockReturnValue({
|
jest.spyOn(model, 'findOneAndUpdate').mockReturnValue({
|
||||||
exec: jest.fn().mockResolvedValueOnce(toUpdated) as any,
|
exec: jest.fn().mockResolvedValue(toUpdated) as any,
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
service.update('5ee49c3115a4e75254bb732e', toUpdated).subscribe({
|
service.update('5ee49c3115a4e75254bb732e', toUpdated).subscribe({
|
||||||
next: (data) => {
|
next: (data) => {
|
||||||
expect(data._id).toBe('5ee49c3115a4e75254bb732e');
|
expect(data).toBeTruthy();
|
||||||
},
|
expect(model.findOneAndUpdate).toBeCalled();
|
||||||
error: (error) => console.log(error),
|
},
|
||||||
complete: done(),
|
error: (error) => console.log(error),
|
||||||
|
complete: done(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throw an NotFoundException if post not exists', (done) => {
|
||||||
|
const toUpdated = {
|
||||||
|
_id: '5ee49c3115a4e75254bb732e',
|
||||||
|
title: 'test title',
|
||||||
|
content: 'test content',
|
||||||
|
};
|
||||||
|
jest.spyOn(model, 'findOneAndUpdate').mockReturnValue({
|
||||||
|
exec: jest.fn().mockResolvedValue(null) as any,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
service.update('5ee49c3115a4e75254bb732e', toUpdated).subscribe({
|
||||||
|
error: (error) => {
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(model.findOneAndUpdate).toHaveBeenCalledTimes(1);
|
||||||
|
},
|
||||||
|
complete: done(),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete post', (done) => {
|
describe('delete', () => {
|
||||||
const toDeleted = {
|
it('perform delete if post exists', (done) => {
|
||||||
_id: '5ee49c3115a4e75254bb732e',
|
const toDeleted = {
|
||||||
title: 'test title',
|
_id: '5ee49c3115a4e75254bb732e',
|
||||||
content: 'test content',
|
title: 'test title',
|
||||||
};
|
content: 'test content',
|
||||||
jest.spyOn(model, 'findOneAndDelete').mockReturnValue({
|
};
|
||||||
exec: jest.fn().mockResolvedValueOnce(toDeleted),
|
jest.spyOn(model, 'findOneAndDelete').mockReturnValue({
|
||||||
} as any);
|
exec: jest.fn().mockResolvedValueOnce(toDeleted),
|
||||||
|
} as any);
|
||||||
|
|
||||||
service.deleteById('anystring').subscribe({
|
service.deleteById('anystring').subscribe({
|
||||||
next: (data) => expect(data._id).toEqual('5ee49c3115a4e75254bb732e'),
|
next: (data) => {
|
||||||
error: (error) => console.log(error),
|
expect(data).toBeTruthy();
|
||||||
complete: done(),
|
expect(model.findOneAndDelete).toBeCalled();
|
||||||
|
},
|
||||||
|
error: (error) => console.log(error),
|
||||||
|
complete: done(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throw an NotFoundException if post not exists', (done) => {
|
||||||
|
jest.spyOn(model, 'findOneAndDelete').mockReturnValue({
|
||||||
|
exec: jest.fn().mockResolvedValue(null),
|
||||||
|
} as any);
|
||||||
|
service.deleteById('anystring').subscribe({
|
||||||
|
error: (error) => {
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(model.findOneAndDelete).toBeCalledTimes(1);
|
||||||
|
},
|
||||||
|
complete: done(),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -240,19 +301,15 @@ describe('PostService', () => {
|
|||||||
callback?: (err: any, res: Comment[]) => void,
|
callback?: (err: any, res: Comment[]) => void,
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
select: jest
|
select: jest.fn().mockReturnValue({
|
||||||
.fn()
|
exec: jest.fn().mockResolvedValue([
|
||||||
.mockReturnValue({
|
{
|
||||||
exec: jest
|
_id: 'test',
|
||||||
.fn()
|
content: 'content',
|
||||||
.mockResolvedValue([
|
post: { _id: '_test_id' },
|
||||||
{
|
},
|
||||||
_id: 'test',
|
] as any),
|
||||||
content: 'content',
|
}),
|
||||||
post: { _id: '_test_id' },
|
|
||||||
}
|
|
||||||
] as any),
|
|
||||||
}),
|
|
||||||
} as any;
|
} as any;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -54,7 +54,7 @@ export class PostServiceStub implements Pick<PostService, keyof PostService> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleteById(id: string): Observable<Post> {
|
deleteById(id: string): Observable<Post> {
|
||||||
return of({ ...this.posts[0], _id: id } as Post);
|
return of({ _id: id, title:'test title', content:'content' } as Post);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteAll(): Observable<any> {
|
deleteAll(): Observable<any> {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Inject, Injectable, Scope } from '@nestjs/common';
|
import { Inject, Injectable, Scope, NotFoundException } from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { Model } from 'mongoose';
|
import { Model } from 'mongoose';
|
||||||
import { from, Observable } from 'rxjs';
|
import { from, Observable, EMPTY, of } from 'rxjs';
|
||||||
import { AuthenticatedRequest } from '../auth/authenticated-request.interface';
|
import { AuthenticatedRequest } from '../auth/authenticated-request.interface';
|
||||||
import { Comment } from '../database/comment.model';
|
import { Comment } from '../database/comment.model';
|
||||||
import { COMMENT_MODEL, POST_MODEL } from '../database/database.constants';
|
import { COMMENT_MODEL, POST_MODEL } from '../database/database.constants';
|
||||||
@ -9,7 +9,7 @@ import { Post } from '../database/post.model';
|
|||||||
import { CreateCommentDto } from './create-comment.dto';
|
import { CreateCommentDto } from './create-comment.dto';
|
||||||
import { CreatePostDto } from './create-post.dto';
|
import { CreatePostDto } from './create-post.dto';
|
||||||
import { UpdatePostDto } from './update-post.dto';
|
import { UpdatePostDto } from './update-post.dto';
|
||||||
|
import { throwIfEmpty, flatMap, filter, map, switchMap } from 'rxjs/operators';
|
||||||
|
|
||||||
@Injectable({ scope: Scope.REQUEST })
|
@Injectable({ scope: Scope.REQUEST })
|
||||||
export class PostService {
|
export class PostService {
|
||||||
@ -29,18 +29,15 @@ export class PostService {
|
|||||||
.exec(),
|
.exec(),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return from(
|
return from(this.postModel.find({}).skip(skip).limit(limit).exec());
|
||||||
this.postModel
|
|
||||||
.find({})
|
|
||||||
.skip(skip)
|
|
||||||
.limit(limit)
|
|
||||||
.exec(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
findById(id: string): Observable<Post> {
|
findById(id: string): Observable<Post> {
|
||||||
return from(this.postModel.findOne({ _id: id }).exec());
|
return from(this.postModel.findOne({ _id: id }).exec()).pipe(
|
||||||
|
flatMap((p) => (p ? of(p) : EMPTY)),
|
||||||
|
throwIfEmpty(() => new NotFoundException(`post:$id was not found`)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
save(data: CreatePostDto): Observable<Post> {
|
save(data: CreatePostDto): Observable<Post> {
|
||||||
@ -58,13 +55,39 @@ export class PostService {
|
|||||||
.findOneAndUpdate(
|
.findOneAndUpdate(
|
||||||
{ _id: id },
|
{ _id: id },
|
||||||
{ ...data, updatedBy: { _id: this.req.user.id } },
|
{ ...data, updatedBy: { _id: this.req.user.id } },
|
||||||
|
{ new: true },
|
||||||
)
|
)
|
||||||
.exec(),
|
.exec(),
|
||||||
|
).pipe(
|
||||||
|
flatMap((p) => (p ? of(p) : EMPTY)),
|
||||||
|
throwIfEmpty(() => new NotFoundException(`post:$id was not found`)),
|
||||||
);
|
);
|
||||||
|
// const filter = { _id: id };
|
||||||
|
// const update = { ...data, updatedBy: { _id: this.req.user.id } };
|
||||||
|
// return from(this.postModel.findOne(filter).exec()).pipe(
|
||||||
|
// flatMap((post) => (post ? of(post) : EMPTY)),
|
||||||
|
// throwIfEmpty(() => new NotFoundException(`post:$id was not found`)),
|
||||||
|
// switchMap((p, i) => {
|
||||||
|
// return from(this.postModel.updateOne(filter, update).exec());
|
||||||
|
// }),
|
||||||
|
// map((res) => res.nModified),
|
||||||
|
// );
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteById(id: string): Observable<Post> {
|
deleteById(id: string): Observable<Post> {
|
||||||
return from(this.postModel.findOneAndDelete({ _id: id }).exec());
|
return from(this.postModel.findOneAndDelete({ _id: id }).exec()).pipe(
|
||||||
|
flatMap((p) => (p ? of(p) : EMPTY)),
|
||||||
|
throwIfEmpty(() => new NotFoundException(`post:$id was not found`)),
|
||||||
|
);
|
||||||
|
// const filter = { _id: id };
|
||||||
|
// return from(this.postModel.findOne(filter).exec()).pipe(
|
||||||
|
// flatMap((post) => (post ? of(post) : EMPTY)),
|
||||||
|
// throwIfEmpty(() => new NotFoundException(`post:$id was not found`)),
|
||||||
|
// switchMap((p, i) => {
|
||||||
|
// return from(this.postModel.deleteOne(filter).exec());
|
||||||
|
// }),
|
||||||
|
// map((res) => res.deletedCount),
|
||||||
|
// );
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteAll(): Observable<any> {
|
deleteAll(): Observable<any> {
|
||||||
|
|||||||
@ -1,4 +1,10 @@
|
|||||||
|
import { IsNotEmpty } from "class-validator";
|
||||||
|
|
||||||
export class UpdatePostDto {
|
export class UpdatePostDto {
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
readonly title: string;
|
readonly title: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
readonly content: string;
|
readonly content: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,31 +5,39 @@ import {
|
|||||||
OnModuleInit,
|
OnModuleInit,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Model } from 'mongoose';
|
import { Model } from 'mongoose';
|
||||||
import { RoleType } from '../database/role-type.enum';
|
|
||||||
import { USER_MODEL } from '../database/database.constants';
|
import { USER_MODEL } from '../database/database.constants';
|
||||||
|
import { RoleType } from '../database/role-type.enum';
|
||||||
import { User } from '../database/user.model';
|
import { User } from '../database/user.model';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserDataInitializerService
|
export class UserDataInitializerService
|
||||||
implements OnModuleInit, OnModuleDestroy {
|
implements OnModuleInit, OnModuleDestroy {
|
||||||
constructor(@Inject(USER_MODEL) private userModel: Model<User>) {
|
constructor(@Inject(USER_MODEL) private userModel: Model<User>) {}
|
||||||
//console.log(`userModel in UserDataInitializerService:${userModel}`);
|
|
||||||
}
|
|
||||||
onModuleInit(): void {
|
onModuleInit(): void {
|
||||||
console.log('(UserModule) is initialized...');
|
console.log('(UserModule) is initialized...');
|
||||||
this.userModel
|
const user = {
|
||||||
.create({
|
username: 'hantsy',
|
||||||
username: 'hantsy',
|
password: 'password',
|
||||||
password: 'password',
|
email: 'hantsy@example.com',
|
||||||
email: 'hantsy@example.com',
|
roles: [RoleType.USER],
|
||||||
roles: [RoleType.USER],
|
};
|
||||||
})
|
|
||||||
.then(data => console.log(data));
|
const admin = {
|
||||||
|
username: 'admin',
|
||||||
|
password: 'password',
|
||||||
|
email: 'admin@example.com',
|
||||||
|
roles: [RoleType.ADMIN],
|
||||||
|
};
|
||||||
|
|
||||||
|
[user, admin].map((u) =>
|
||||||
|
this.userModel.create(u).then((data) => console.log(data)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onModuleDestroy(): void {
|
onModuleDestroy(): void {
|
||||||
console.log('(UserModule) is being destroyed...');
|
this.userModel.deleteMany({}).then((del) => {
|
||||||
this.userModel
|
console.log(`deleted ${del.deletedCount} rows`);
|
||||||
.deleteMany({})
|
});
|
||||||
.then(del => console.log(`deleted ${del.deletedCount} rows`));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,24 +1,186 @@
|
|||||||
|
import { INestApplication, ValidationPipe } from '@nestjs/common';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { INestApplication } from '@nestjs/common';
|
|
||||||
import * as request from 'supertest';
|
import * as request from 'supertest';
|
||||||
import { AppModule } from './../src/app.module';
|
import { AppModule } from './../src/app.module';
|
||||||
|
import * as mongoose from 'mongoose';
|
||||||
|
|
||||||
describe('AppController (e2e)', () => {
|
describe('API endpoints testing (e2e)', () => {
|
||||||
let app: INestApplication;
|
let app: INestApplication;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeAll(async () => {
|
||||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
imports: [AppModule],
|
imports: [AppModule],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
app = moduleFixture.createNestApplication();
|
app = moduleFixture.createNestApplication();
|
||||||
|
app.enableShutdownHooks();
|
||||||
|
app.useGlobalPipes(new ValidationPipe());
|
||||||
await app.init();
|
await app.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('/ (GET)', () => {
|
afterAll(async () => {
|
||||||
return request(app.getHttpServer())
|
await app.close();
|
||||||
.get('/')
|
});
|
||||||
.expect(200)
|
|
||||||
.expect('Hello World!');
|
describe('if user is not logged in', () => {
|
||||||
|
it('/posts (GET)', async () => {
|
||||||
|
const res = await request(app.getHttpServer()).get('/posts').send();
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.length).toEqual(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/posts (GET) if none existing should return 404', async () => {
|
||||||
|
const id = mongoose.Types.ObjectId();
|
||||||
|
const res = await request(app.getHttpServer()).get('/posts/' + id);
|
||||||
|
expect(res.status).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/posts (GET) if invalid id should return 400', async () => {
|
||||||
|
const id = "invalidid";
|
||||||
|
const res = await request(app.getHttpServer()).get('/posts/' + id);
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/posts (POST) should return 401', async () => {
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.post('/posts')
|
||||||
|
.send({ title: 'test title', content: 'test content' });
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/posts (PUT) should return 401', async () => {
|
||||||
|
const id = mongoose.Types.ObjectId();
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.put('/posts/' + id)
|
||||||
|
.send({ title: 'test title', content: 'test content' });
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/posts (DELETE) should return 401', async () => {
|
||||||
|
const id = mongoose.Types.ObjectId();
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.delete('/posts/' + id)
|
||||||
|
.send();
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if user is logged in as (USER)', () => {
|
||||||
|
let jwttoken: any;
|
||||||
|
beforeEach(async () => {
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.post('/auth/login')
|
||||||
|
.send({ username: 'hantsy', password: 'password' });
|
||||||
|
|
||||||
|
expect(res.status).toBe(201);
|
||||||
|
jwttoken = res.body.access_token;
|
||||||
|
//console.log(jwttoken);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/posts (GET)', async () => {
|
||||||
|
const res = await request(app.getHttpServer()).get('/posts');
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.length).toEqual(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/posts (POST) with empty body should return 400', async () => {
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.post('/posts')
|
||||||
|
.set('Authorization', 'Bearer ' + jwttoken)
|
||||||
|
.send({});
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/posts (PUT) if none existing should return 404', async () => {
|
||||||
|
const id = mongoose.Types.ObjectId();
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.put('/posts/' + id)
|
||||||
|
.set('Authorization', 'Bearer ' + jwttoken)
|
||||||
|
.send({ title: 'test title', content: 'test content' });
|
||||||
|
expect(res.status).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/posts (DELETE) if none existing should return 403', async () => {
|
||||||
|
const id = mongoose.Types.ObjectId();
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.delete('/posts/' + id)
|
||||||
|
.set('Authorization', 'Bearer ' + jwttoken)
|
||||||
|
.send();
|
||||||
|
expect(res.status).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/posts crud flow', async () => {
|
||||||
|
//create a post
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.post('/posts')
|
||||||
|
.set('Authorization', 'Bearer ' + jwttoken)
|
||||||
|
.send({ title: 'test title', content: 'test content' });
|
||||||
|
expect(res.status).toBe(201);
|
||||||
|
const saveduri = res.get('Location');
|
||||||
|
//console.log(saveduri);
|
||||||
|
|
||||||
|
// get the saved post
|
||||||
|
const resget = await request(app.getHttpServer()).get(saveduri);
|
||||||
|
expect(resget.status).toBe(200);
|
||||||
|
expect(resget.body.title).toBe('test title');
|
||||||
|
expect(resget.body.content).toBe('test content');
|
||||||
|
expect(resget.body.createdAt).toBeDefined();
|
||||||
|
|
||||||
|
// update the post
|
||||||
|
const updateres = await request(app.getHttpServer())
|
||||||
|
.put(saveduri)
|
||||||
|
.set('Authorization', 'Bearer ' + jwttoken)
|
||||||
|
.send({ title: 'updated title', content: 'updated content' });
|
||||||
|
expect(updateres.status).toBe(204);
|
||||||
|
|
||||||
|
// verify the updated post
|
||||||
|
const updatedres = await request(app.getHttpServer()).get(saveduri);
|
||||||
|
expect(updatedres.status).toBe(200);
|
||||||
|
expect(updatedres.body.title).toBe('updated title');
|
||||||
|
expect(updatedres.body.content).toBe('updated content');
|
||||||
|
expect(updatedres.body.updatedAt).toBeDefined();
|
||||||
|
|
||||||
|
// creat a comment
|
||||||
|
const commentres = await request(app.getHttpServer())
|
||||||
|
.post(saveduri + '/comments')
|
||||||
|
.set('Authorization', 'Bearer ' + jwttoken)
|
||||||
|
.send({ content: 'test content' });
|
||||||
|
expect(commentres.status).toBe(201);
|
||||||
|
expect(commentres.get('Location')).toBeTruthy();
|
||||||
|
|
||||||
|
// get the comments of post
|
||||||
|
const getCommentsRes = await request(app.getHttpServer()).get(
|
||||||
|
saveduri + '/comments',
|
||||||
|
);
|
||||||
|
expect(getCommentsRes.status).toBe(200);
|
||||||
|
expect(getCommentsRes.body.length).toEqual(1);
|
||||||
|
|
||||||
|
// delete the posts
|
||||||
|
const deleteRes = await request(app.getHttpServer())
|
||||||
|
.delete(saveduri)
|
||||||
|
.set('Authorization', 'Bearer ' + jwttoken)
|
||||||
|
.send();
|
||||||
|
expect(deleteRes.status).toBe(403);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if user is logged in as (ADMIN)', () => {
|
||||||
|
let jwttoken: any;
|
||||||
|
beforeEach(async () => {
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.post('/auth/login')
|
||||||
|
.send({ username: 'admin', password: 'password' });
|
||||||
|
jwttoken = res.body.access_token;
|
||||||
|
// console.log(jwttoken);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/posts (DELETE) if none existing should return 404', async () => {
|
||||||
|
const id = mongoose.Types.ObjectId();
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.delete('/posts/' + id)
|
||||||
|
.set('Authorization', 'Bearer ' + jwttoken)
|
||||||
|
.send();
|
||||||
|
expect(res.status).toBe(404);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user