mirror of
https://github.com/hantsy/nestjs-rest-sample.git
synced 2025-12-08 20:36:27 +00:00
feat(config): add config module, and more testing codes
This commit is contained in:
parent
5bb4d4300a
commit
04d9ef758b
5
codecov.yml
Normal file
5
codecov.yml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
ignore:
|
||||||
|
- "**/*stub.ts"
|
||||||
|
- "**/*dto.ts"
|
||||||
|
- "**/*interface.ts"
|
||||||
|
- "**/*model.ts"
|
||||||
38
package-lock.json
generated
38
package-lock.json
generated
@ -1189,6 +1189,12 @@
|
|||||||
"integrity": "sha512-wo2rHprtDzTHf4tiSxavktJ52ntiwmg7eHNGFLH38G1of8OfGVwOc1sVbpM4jN/HK/rCMhYOi6xzoPqsv0537A==",
|
"integrity": "sha512-wo2rHprtDzTHf4tiSxavktJ52ntiwmg7eHNGFLH38G1of8OfGVwOc1sVbpM4jN/HK/rCMhYOi6xzoPqsv0537A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@golevelup/nestjs-testing": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/@golevelup/nestjs-testing/download/@golevelup/nestjs-testing-0.1.2.tgz",
|
||||||
|
"integrity": "sha1-MU6H5jTVgXl9CGyfX0Xc6AKtkZo=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@istanbuljs/load-nyc-config": {
|
"@istanbuljs/load-nyc-config": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
||||||
@ -2192,6 +2198,18 @@
|
|||||||
"uuid": "8.2.0"
|
"uuid": "8.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@nestjs/config": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/@nestjs/config/download/@nestjs/config-0.5.0.tgz",
|
||||||
|
"integrity": "sha1-rREQ2TfsJpQbj85fV1hZBG+Ne0s=",
|
||||||
|
"requires": {
|
||||||
|
"dotenv": "8.2.0",
|
||||||
|
"dotenv-expand": "5.1.0",
|
||||||
|
"lodash.get": "4.4.2",
|
||||||
|
"lodash.set": "4.3.2",
|
||||||
|
"uuid": "8.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@nestjs/core": {
|
"@nestjs/core": {
|
||||||
"version": "7.3.1",
|
"version": "7.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-7.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-7.3.1.tgz",
|
||||||
@ -5182,6 +5200,16 @@
|
|||||||
"is-obj": "^1.0.0"
|
"is-obj": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dotenv": {
|
||||||
|
"version": "8.2.0",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/dotenv/download/dotenv-8.2.0.tgz",
|
||||||
|
"integrity": "sha1-l+YZJZradQ7qPk6j4mvO6lQksWo="
|
||||||
|
},
|
||||||
|
"dotenv-expand": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/dotenv-expand/download/dotenv-expand-5.1.0.tgz",
|
||||||
|
"integrity": "sha1-P7rwIL/XlIhAcuomsel5HUWmKfA="
|
||||||
|
},
|
||||||
"duplexify": {
|
"duplexify": {
|
||||||
"version": "3.7.1",
|
"version": "3.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
|
||||||
@ -10370,6 +10398,11 @@
|
|||||||
"integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=",
|
"integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"lodash.get": {
|
||||||
|
"version": "4.4.2",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/lodash.get/download/lodash.get-4.4.2.tgz",
|
||||||
|
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
|
||||||
|
},
|
||||||
"lodash.includes": {
|
"lodash.includes": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npm.taobao.org/lodash.includes/download/lodash.includes-4.3.0.tgz",
|
"resolved": "https://registry.npm.taobao.org/lodash.includes/download/lodash.includes-4.3.0.tgz",
|
||||||
@ -10411,6 +10444,11 @@
|
|||||||
"resolved": "https://registry.npm.taobao.org/lodash.once/download/lodash.once-4.1.1.tgz",
|
"resolved": "https://registry.npm.taobao.org/lodash.once/download/lodash.once-4.1.1.tgz",
|
||||||
"integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
|
"integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
|
||||||
},
|
},
|
||||||
|
"lodash.set": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/lodash.set/download/lodash.set-4.3.2.tgz",
|
||||||
|
"integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM="
|
||||||
|
},
|
||||||
"lodash.sortby": {
|
"lodash.sortby": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
|
||||||
|
|||||||
@ -22,6 +22,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^7.2.0",
|
"@nestjs/common": "^7.2.0",
|
||||||
|
"@nestjs/config": "^0.5.0",
|
||||||
"@nestjs/core": "^7.2.0",
|
"@nestjs/core": "^7.2.0",
|
||||||
"@nestjs/jwt": "^7.0.0",
|
"@nestjs/jwt": "^7.0.0",
|
||||||
"@nestjs/passport": "^7.1.0",
|
"@nestjs/passport": "^7.1.0",
|
||||||
@ -37,6 +38,7 @@
|
|||||||
"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",
|
||||||
"@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.2.0",
|
||||||
@ -46,8 +48,8 @@
|
|||||||
"@types/node": "^14.0.14",
|
"@types/node": "^14.0.14",
|
||||||
"@types/passport-jwt": "^3.0.3",
|
"@types/passport-jwt": "^3.0.3",
|
||||||
"@types/passport-local": "^1.0.33",
|
"@types/passport-local": "^1.0.33",
|
||||||
"@typescript-eslint/eslint-plugin": "3.5.0",
|
|
||||||
"@types/supertest": "^2.0.10",
|
"@types/supertest": "^2.0.10",
|
||||||
|
"@typescript-eslint/eslint-plugin": "3.5.0",
|
||||||
"@typescript-eslint/parser": "3.5.0",
|
"@typescript-eslint/parser": "3.5.0",
|
||||||
"eslint": "7.3.1",
|
"eslint": "7.3.1",
|
||||||
"eslint-config-prettier": "^6.10.0",
|
"eslint-config-prettier": "^6.10.0",
|
||||||
|
|||||||
@ -1,37 +1,17 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AuthService } from './auth/auth.service';
|
|
||||||
import { of } from 'rxjs';
|
|
||||||
|
|
||||||
describe('AppController', () => {
|
describe('AppController', () => {
|
||||||
let appController: AppController;
|
let appController: AppController;
|
||||||
let authService: AuthService;
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const app: TestingModule = await Test.createTestingModule({
|
const app: TestingModule = await Test.createTestingModule({
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: AuthService,
|
|
||||||
useValue: {
|
|
||||||
constructor: jest.fn(),
|
|
||||||
login: jest.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
appController = app.get<AppController>(AppController);
|
appController = app.get<AppController>(AppController);
|
||||||
authService = app.get<AuthService>(AuthService);
|
|
||||||
});
|
});
|
||||||
|
it('should be defined', () => {
|
||||||
describe('root', () => {
|
expect(appController).toBeDefined();
|
||||||
it('should return access_token', async() => {
|
|
||||||
jest.spyOn(authService, "login").mockImplementation((user:any)=>{
|
|
||||||
return of({ access_token: 'jwttoken' } );
|
|
||||||
});
|
|
||||||
const token = await appController.login({} as any).toPromise();
|
|
||||||
expect(token.access_token).toEqual('jwttoken');
|
|
||||||
expect(authService.login).toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,24 +1,6 @@
|
|||||||
import { Controller, Post, UseGuards, Get, Req } from '@nestjs/common';
|
import { Controller } from '@nestjs/common';
|
||||||
import { Request } from 'express';
|
|
||||||
import { LocalAuthGuard } from './auth/local-auth.guard';
|
|
||||||
import { JwtAuthGuard } from './auth/jwt-auth.guard';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { AuthService } from './auth/auth.service';
|
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class AppController {
|
export class AppController {
|
||||||
constructor(private authService: AuthService) {}
|
|
||||||
|
|
||||||
@UseGuards(LocalAuthGuard)
|
|
||||||
@Post('auth/login')
|
|
||||||
login(@Req() req: Request): Observable<any> {
|
|
||||||
return this.authService.login(req.user);
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@Get('profile')
|
|
||||||
getProfile(@Req() req: Request): any {
|
|
||||||
//console.log(req.user);
|
|
||||||
return req.user;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
@ -8,6 +9,7 @@ import { UserModule } from './user/user.module';
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
ConfigModule.forRoot({ ignoreEnvFile: true }),
|
||||||
DatabaseModule,
|
DatabaseModule,
|
||||||
PostModule,
|
PostModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
|
|||||||
3
src/auth/access-token.interface.ts
Normal file
3
src/auth/access-token.interface.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export interface AccessToken{
|
||||||
|
readonly access_token:string
|
||||||
|
}
|
||||||
@ -1,5 +1 @@
|
|||||||
export const jwtConstants = {
|
|
||||||
secret: 'secretKey',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const HAS_ROLES_KEY = 'has-roles';
|
export const HAS_ROLES_KEY = 'has-roles';
|
||||||
|
|||||||
37
src/auth/auth.controller.spec.ts
Normal file
37
src/auth/auth.controller.spec.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { AuthController } from './auth.controller';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
|
describe('AuthController', () => {
|
||||||
|
let controller: AuthController;
|
||||||
|
let authService: AuthService;
|
||||||
|
beforeEach(async () => {
|
||||||
|
const app: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [AuthController],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: AuthService,
|
||||||
|
useValue: {
|
||||||
|
constructor: jest.fn(),
|
||||||
|
login: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = app.get<AuthController>(AuthController);
|
||||||
|
authService = app.get<AuthService>(AuthService);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('login', () => {
|
||||||
|
it('should return access_token', async () => {
|
||||||
|
jest.spyOn(authService, 'login').mockImplementation((user: any) => {
|
||||||
|
return of({ access_token: 'jwttoken' });
|
||||||
|
});
|
||||||
|
const token = await controller.login({} as any).toPromise();
|
||||||
|
expect(token.access_token).toEqual('jwttoken');
|
||||||
|
expect(authService.login).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
17
src/auth/auth.controller.ts
Normal file
17
src/auth/auth.controller.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Controller, UseGuards, Post, Req } from '@nestjs/common';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { LocalAuthGuard } from './local-auth.guard';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { Request } from 'express';
|
||||||
|
import { AuthenticatedRequest } from './authenticated-request.interface';
|
||||||
|
|
||||||
|
@Controller('auth')
|
||||||
|
export class AuthController {
|
||||||
|
constructor(private authService: AuthService) {}
|
||||||
|
|
||||||
|
@UseGuards(LocalAuthGuard)
|
||||||
|
@Post('login')
|
||||||
|
login(@Req() req: AuthenticatedRequest): Observable<any> {
|
||||||
|
return this.authService.login(req.user);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,20 +3,30 @@ import { AuthService } from './auth.service';
|
|||||||
import { UserModule } from '../user/user.module';
|
import { UserModule } from '../user/user.module';
|
||||||
import { PassportModule } from '@nestjs/passport';
|
import { PassportModule } from '@nestjs/passport';
|
||||||
import { LocalStrategy } from './local.strategy';
|
import { LocalStrategy } from './local.strategy';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule, JwtModuleOptions } from '@nestjs/jwt';
|
||||||
import { jwtConstants } from './auth.constants';
|
|
||||||
import { JwtStrategy } from './jwt.strategy';
|
import { JwtStrategy } from './jwt.strategy';
|
||||||
|
import { ConfigModule, ConfigType } from '@nestjs/config';
|
||||||
|
import jwtConfig from '../config/jwt.config';
|
||||||
|
import { AuthController } from './auth.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
ConfigModule.forFeature(jwtConfig),
|
||||||
UserModule,
|
UserModule,
|
||||||
PassportModule.register({ defaultStrategy: 'jwt' }),
|
PassportModule.register({ defaultStrategy: 'jwt' }),
|
||||||
JwtModule.register({
|
JwtModule.registerAsync({
|
||||||
secret: jwtConstants.secret,
|
imports: [ConfigModule.forFeature(jwtConfig)],
|
||||||
signOptions: { expiresIn: '3600s' },
|
useFactory: (config: ConfigType<typeof jwtConfig>) => {
|
||||||
|
return {
|
||||||
|
secret: config.secretKey,
|
||||||
|
signOptions: { expiresIn: config.expiresIn },
|
||||||
|
} as JwtModuleOptions;
|
||||||
|
},
|
||||||
|
inject: [jwtConfig.KEY],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
providers: [AuthService, LocalStrategy, JwtStrategy],
|
providers: [AuthService, LocalStrategy, JwtStrategy],
|
||||||
exports: [AuthService],
|
exports: [AuthService],
|
||||||
|
controllers: [AuthController],
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
|
|||||||
@ -1,10 +1,16 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { AuthService } from './auth.service';
|
|
||||||
import { UserService } from '../user/user.service';
|
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { toArray } from 'rxjs/operators';
|
||||||
|
import { RoleType } from '../database/role-type.enum';
|
||||||
|
import { User } from '../database/user.model';
|
||||||
|
import { UserService } from '../user/user.service';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
describe('AuthService', () => {
|
describe('AuthService', () => {
|
||||||
let service: AuthService;
|
let service: AuthService;
|
||||||
|
let userService: UserService;
|
||||||
|
let jwtService: JwtService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
@ -28,9 +34,108 @@ describe('AuthService', () => {
|
|||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<AuthService>(AuthService);
|
service = module.get<AuthService>(AuthService);
|
||||||
|
userService = module.get<UserService>(UserService);
|
||||||
|
jwtService = module.get<JwtService>(JwtService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
expect(service).toBeDefined();
|
expect(service).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('validateUser', () => {
|
||||||
|
it('if user is found', async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(userService, 'findByUsername')
|
||||||
|
.mockImplementation((username: string) => {
|
||||||
|
return of({
|
||||||
|
username,
|
||||||
|
password: 'password',
|
||||||
|
email: 'hantsy@example.com',
|
||||||
|
roles: [RoleType.USER],
|
||||||
|
} as User);
|
||||||
|
});
|
||||||
|
|
||||||
|
service.validateUser('test', 'password').subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
expect(data.username).toBe('test');
|
||||||
|
// expect(data.password).toBeUndefined();
|
||||||
|
expect(data.email).toBe('hantsy@example.com');
|
||||||
|
expect(data.roles).toEqual([RoleType.USER]);
|
||||||
|
|
||||||
|
//verify
|
||||||
|
expect(userService.findByUsername).toBeCalledTimes(1);
|
||||||
|
expect(userService.findByUsername).toBeCalledWith('test');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if user is found but pass is mismatched', async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(userService, 'findByUsername')
|
||||||
|
.mockImplementation((username: string) => {
|
||||||
|
return of({
|
||||||
|
username,
|
||||||
|
password: 'password',
|
||||||
|
email: 'hantsy@example.com',
|
||||||
|
roles: [RoleType.USER],
|
||||||
|
} as User);
|
||||||
|
});
|
||||||
|
|
||||||
|
service
|
||||||
|
.validateUser('test', 'password001')
|
||||||
|
.pipe(toArray())
|
||||||
|
.subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
expect(data.length).toBe(0);
|
||||||
|
expect(userService.findByUsername).toBeCalledTimes(1);
|
||||||
|
expect(userService.findByUsername).toBeCalledWith('test');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if user is not found', async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(userService, 'findByUsername')
|
||||||
|
.mockImplementation((username: string) => {
|
||||||
|
return of(null as User);
|
||||||
|
});
|
||||||
|
|
||||||
|
service
|
||||||
|
.validateUser('test', 'password001')
|
||||||
|
.pipe(toArray())
|
||||||
|
.subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
expect(data.length).toBe(0);
|
||||||
|
expect(userService.findByUsername).toBeCalledTimes(1);
|
||||||
|
expect(userService.findByUsername).toBeCalledWith('test');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('login', () => {
|
||||||
|
it('should return signed jwt token', async () => {
|
||||||
|
jest.spyOn(jwtService, 'signAsync').mockResolvedValue('test');
|
||||||
|
|
||||||
|
service
|
||||||
|
.login({
|
||||||
|
username: 'test',
|
||||||
|
id: '_id',
|
||||||
|
email: 'hantsy@example.com',
|
||||||
|
roles: [RoleType.USER],
|
||||||
|
})
|
||||||
|
.subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
expect(data.access_token).toBe('test');
|
||||||
|
expect(jwtService.signAsync).toBeCalledTimes(1);
|
||||||
|
expect(jwtService.signAsync).toBeCalledWith({
|
||||||
|
upn: 'test',
|
||||||
|
sub: '_id',
|
||||||
|
email: 'hantsy@example.com',
|
||||||
|
roles: [RoleType.USER],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,6 +3,9 @@ import { JwtService } from '@nestjs/jwt';
|
|||||||
import { from, Observable, EMPTY, of } from 'rxjs';
|
import { from, Observable, EMPTY, of } from 'rxjs';
|
||||||
import { map, flatMap } from 'rxjs/operators';
|
import { map, flatMap } from 'rxjs/operators';
|
||||||
import { UserService } from '../user/user.service';
|
import { UserService } from '../user/user.service';
|
||||||
|
import { UserPrincipal } from './user-principal.interface';
|
||||||
|
import { JwtPayload } from './jwt-payload.interface';
|
||||||
|
import { AccessToken } from './access-token.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
@ -11,13 +14,13 @@ export class AuthService {
|
|||||||
private jwtService: JwtService,
|
private jwtService: JwtService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
validateUser(username: string, pass: string): Observable<any> {
|
validateUser(username: string, pass: string): Observable<UserPrincipal> {
|
||||||
return this.userService.findByUsername(username).pipe(
|
return this.userService.findByUsername(username).pipe(
|
||||||
flatMap(user => {
|
flatMap((user) => {
|
||||||
//console.log('userService.findByUsername::' + JSON.stringify(user));
|
//console.log('userService.findByUsername::' + JSON.stringify(user));
|
||||||
if (user && user.password === pass) {
|
if (user && user.password === pass) {
|
||||||
const { _id, username, email, roles } = user;
|
const { _id, username, email, roles } = user;
|
||||||
return of({ _id, username, email, roles });
|
return of({ id: _id, username, email, roles });
|
||||||
}
|
}
|
||||||
return EMPTY;
|
return EMPTY;
|
||||||
}),
|
}),
|
||||||
@ -25,20 +28,21 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If `LocalStrateg#validateUser` return a `Observable`, the `request.user` is
|
// If `LocalStrateg#validateUser` return a `Observable`, the `request.user` is
|
||||||
// bound to a `Observable<User>`, not a `User`.
|
// bound to a `Observable<UserPrincipal>`, not a `UserPrincipal`.
|
||||||
//
|
//
|
||||||
// I would like use the current `Promise` for this case.
|
// I would like use the current `Promise` for this case, thus it will get
|
||||||
|
// a `UserPrincipal` here directly.
|
||||||
//
|
//
|
||||||
login(user: any): Observable<any> {
|
login(user: UserPrincipal): Observable<AccessToken> {
|
||||||
console.log(user);
|
//console.log(user);
|
||||||
const payload = {
|
const payload: JwtPayload = {
|
||||||
upn: user.username, //upn is defined in Microprofile JWT spec, a human readable principal name.
|
upn: user.username, //upn is defined in Microprofile JWT spec, a human readable principal name.
|
||||||
sub: user._id,
|
sub: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
roles: user.roles,
|
roles: user.roles,
|
||||||
};
|
};
|
||||||
return from(this.jwtService.signAsync(payload)).pipe(
|
return from(this.jwtService.signAsync(payload)).pipe(
|
||||||
map(access_token => {
|
map((access_token) => {
|
||||||
return { access_token };
|
return { access_token };
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { User } from 'src/database/user.model';
|
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
|
import { UserPrincipal } from './user-principal.interface';
|
||||||
|
|
||||||
export interface AuthenticatedRequest extends Request {
|
export interface AuthenticatedRequest extends Request {
|
||||||
readonly user: User;
|
readonly user: UserPrincipal;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,51 @@
|
|||||||
|
import { createMock } from '@golevelup/nestjs-testing';
|
||||||
|
import { ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { JwtAuthGuard } from './jwt-auth.guard';
|
import { JwtAuthGuard } from './jwt-auth.guard';
|
||||||
|
|
||||||
describe('JwtAuthGuard', () => {
|
describe('LocalAuthGuard', () => {
|
||||||
|
let guard: JwtAuthGuard;
|
||||||
|
beforeEach(() => {
|
||||||
|
guard = new JwtAuthGuard();
|
||||||
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
expect(new JwtAuthGuard()).toBeDefined();
|
expect(guard).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for `canActivate`', async () => {
|
||||||
|
AuthGuard('jwt').prototype.canActivate = jest.fn(() =>
|
||||||
|
Promise.resolve(true),
|
||||||
|
);
|
||||||
|
AuthGuard('jwt').prototype.logIn = jest.fn(() => Promise.resolve());
|
||||||
|
expect(
|
||||||
|
await guard.canActivate(createMock<ExecutionContext>()),
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handleRequest: error', async () => {
|
||||||
|
const error = { name: 'test', message: 'error' } as Error;
|
||||||
|
|
||||||
|
try {
|
||||||
|
guard.handleRequest(error, {}, {});
|
||||||
|
} catch (e) {
|
||||||
|
//console.log(e);
|
||||||
|
expect(e).toEqual(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handleRequest', async () => {
|
||||||
|
expect(
|
||||||
|
await guard.handleRequest(undefined, { username: 'hantsy' }, undefined),
|
||||||
|
).toEqual({ username: 'hantsy' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handleRequest: Unauthorized', async () => {
|
||||||
|
try {
|
||||||
|
guard.handleRequest(undefined, undefined, undefined);
|
||||||
|
} catch (e) {
|
||||||
|
// console.log(e);
|
||||||
|
expect(e).toBeDefined();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
8
src/auth/jwt-payload.interface.ts
Normal file
8
src/auth/jwt-payload.interface.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { RoleType } from "../database/role-type.enum";
|
||||||
|
|
||||||
|
export interface JwtPayload {
|
||||||
|
readonly upn: string;
|
||||||
|
readonly sub: string;
|
||||||
|
readonly email: string;
|
||||||
|
readonly roles: RoleType[];
|
||||||
|
}
|
||||||
@ -1,27 +1,29 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
import { jwtConstants } from './auth.constants';
|
import jwtConfig from '../config/jwt.config';
|
||||||
|
import { ConfigType } from '@nestjs/config';
|
||||||
|
import { JwtPayload } from './jwt-payload.interface';
|
||||||
|
import { UserPrincipal } from './user-principal.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
constructor() {
|
constructor(@Inject(jwtConfig.KEY) config: ConfigType<typeof jwtConfig>) {
|
||||||
super({
|
super({
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
ignoreExpiration: false,
|
ignoreExpiration: false,
|
||||||
secretOrKey: jwtConstants.secret,
|
secretOrKey: config.secretKey,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//payload is the decoded jwt clmais.
|
//payload is the decoded jwt clmais.
|
||||||
validate(payload: any): any {
|
validate(payload: JwtPayload): UserPrincipal {
|
||||||
//console.log('jwt payload:' + JSON.stringify(payload));
|
//console.log('jwt payload:' + JSON.stringify(payload));
|
||||||
return {
|
return {
|
||||||
username: payload.upn,
|
username: payload.upn,
|
||||||
email: payload.email,
|
email: payload.email,
|
||||||
_id: payload.sub,
|
id: payload.sub,
|
||||||
roles: payload.roles,
|
roles: payload.roles,
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,21 @@
|
|||||||
|
import { ExecutionContext } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { LocalAuthGuard } from './local-auth.guard';
|
import { LocalAuthGuard } from './local-auth.guard';
|
||||||
|
|
||||||
describe('LocalAuthGuard', () => {
|
describe('LocalAuthGuard', () => {
|
||||||
it('should be defined', () => {
|
let guard: LocalAuthGuard;
|
||||||
expect(new LocalAuthGuard()).toBeDefined();
|
beforeEach(() => {
|
||||||
|
guard = new LocalAuthGuard();
|
||||||
});
|
});
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(guard).toBeDefined();
|
||||||
|
});
|
||||||
|
it('should return true for `canActivate`', async () => {
|
||||||
|
AuthGuard('local').prototype.canActivate = jest.fn(() =>
|
||||||
|
Promise.resolve(true),
|
||||||
|
);
|
||||||
|
AuthGuard('local').prototype.logIn = jest.fn(() => Promise.resolve());
|
||||||
|
expect(await guard.canActivate({} as ExecutionContext)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
46
src/auth/local.strategy.spec.ts
Normal file
46
src/auth/local.strategy.spec.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { RoleType } from '../database/role-type.enum';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { LocalStrategy } from './local.strategy';
|
||||||
|
|
||||||
|
describe('LocalStrategy', () => {
|
||||||
|
let strategy: LocalStrategy;
|
||||||
|
let authService: AuthService;
|
||||||
|
beforeEach(async () => {
|
||||||
|
const app: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
LocalStrategy,
|
||||||
|
{
|
||||||
|
provide: AuthService,
|
||||||
|
useValue: {
|
||||||
|
constructor: jest.fn(),
|
||||||
|
login: jest.fn(),
|
||||||
|
validateUser:jest.fn()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
strategy = app.get<LocalStrategy>(LocalStrategy);
|
||||||
|
authService = app.get<AuthService>(AuthService);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validate', () => {
|
||||||
|
it('should return user principal if user and password is provided ', async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(authService, 'validateUser')
|
||||||
|
.mockImplementation((user: any, pass: any) => {
|
||||||
|
return of({
|
||||||
|
username: 'test',
|
||||||
|
id: '_id',
|
||||||
|
email: 'hantsy@example.com',
|
||||||
|
roles: [RoleType.USER],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const user = await strategy.validate('test', 'pass');
|
||||||
|
expect(user.username).toEqual('test');
|
||||||
|
expect(authService.validateUser).toBeCalledWith('test', 'pass');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,9 +1,8 @@
|
|||||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
import { Strategy } from 'passport-local';
|
import { Strategy } from 'passport-local';
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { map, throwIfEmpty } from 'rxjs/operators';
|
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
|
import { UserPrincipal } from './user-principal.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LocalStrategy extends PassportStrategy(Strategy) {
|
export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||||
@ -30,8 +29,8 @@ export class LocalStrategy extends PassportStrategy(Strategy) {
|
|||||||
// .pipe(throwIfEmpty(() => new UnauthorizedException()));
|
// .pipe(throwIfEmpty(() => new UnauthorizedException()));
|
||||||
// }
|
// }
|
||||||
|
|
||||||
async validate(username: string, password: string): Promise<any> {
|
async validate(username: string, password: string): Promise<UserPrincipal> {
|
||||||
const user = await this.authService
|
const user: UserPrincipal = await this.authService
|
||||||
.validateUser(username, password)
|
.validateUser(username, password)
|
||||||
.toPromise();
|
.toPromise();
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,85 @@
|
|||||||
import { RolesGuard } from './roles.guard';
|
import { RolesGuard } from './roles.guard';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { TestingModule, Test } from '@nestjs/testing';
|
||||||
|
import { ExecutionContext } from '@nestjs/common';
|
||||||
|
import { createMock } from '@golevelup/nestjs-testing';
|
||||||
|
import { RoleType } from '../database/role-type.enum';
|
||||||
|
import { AuthenticatedRequest } from './authenticated-request.interface';
|
||||||
|
|
||||||
describe('RolesGuard', () => {
|
describe('RolesGuard', () => {
|
||||||
|
let guard: RolesGuard;
|
||||||
|
let reflector: Reflector;
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
RolesGuard,
|
||||||
|
{
|
||||||
|
provide: Reflector,
|
||||||
|
useValue: {
|
||||||
|
constructor: jest.fn(),
|
||||||
|
get: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
guard = module.get<RolesGuard>(RolesGuard);
|
||||||
|
reflector = module.get<Reflector>(Reflector);
|
||||||
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
//expect(new RolesGuard()).toBeDefined();
|
expect(guard).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip(return true) if the `HasRoles` decorator is not set', async () => {
|
||||||
|
jest.spyOn(reflector, 'get').mockImplementation((a: any, b: any) => {
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
guard.canActivate(
|
||||||
|
createMock<ExecutionContext>({
|
||||||
|
getHandler: jest.fn(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
expect(reflector.get).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if the `HasRoles` decorator is set', async () => {
|
||||||
|
jest.spyOn(reflector, 'get').mockImplementation((a: any, b: any) => {
|
||||||
|
return [RoleType.USER];
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
guard.canActivate(
|
||||||
|
createMock<ExecutionContext>({
|
||||||
|
getHandler: jest.fn(),
|
||||||
|
switchToHttp: jest.fn().mockReturnValue({
|
||||||
|
getRequest: jest.fn().mockReturnValue({
|
||||||
|
user: { roles: [RoleType.USER]},
|
||||||
|
} as AuthenticatedRequest),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
expect(reflector.get).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
return [RoleType.ADMIN];
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
guard.canActivate(
|
||||||
|
createMock<ExecutionContext>({
|
||||||
|
getHandler: jest.fn(),
|
||||||
|
switchToHttp: jest.fn().mockReturnValue({
|
||||||
|
getRequest: jest.fn().mockReturnValue({
|
||||||
|
user: { roles: [RoleType.USER]},
|
||||||
|
} as AuthenticatedRequest),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
expect(reflector.get).toBeCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
8
src/auth/user-principal.interface.ts
Normal file
8
src/auth/user-principal.interface.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { RoleType } from '../database/role-type.enum';
|
||||||
|
|
||||||
|
export interface UserPrincipal {
|
||||||
|
readonly username: string;
|
||||||
|
readonly id: string;
|
||||||
|
readonly email: string;
|
||||||
|
readonly roles: RoleType[];
|
||||||
|
}
|
||||||
23
src/config/jwt.config.spec.ts
Normal file
23
src/config/jwt.config.spec.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ConfigModule, ConfigType } from '@nestjs/config';
|
||||||
|
import { TestingModule, Test } from '@nestjs/testing';
|
||||||
|
import jwtConfig from './jwt.config';
|
||||||
|
|
||||||
|
describe('jwtConfig', () => {
|
||||||
|
let config: ConfigType<typeof jwtConfig>;
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [ConfigModule.forFeature(jwtConfig)],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
config = module.get<ConfigType<typeof jwtConfig>>(jwtConfig.KEY);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(jwtConfig).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contains expiresIn and secret key', async () => {
|
||||||
|
expect(config.expiresIn).toBe('3600s');
|
||||||
|
expect(config.secretKey).toBe('rzxlszyykpbgqcflzxsqcysyhljt');
|
||||||
|
});
|
||||||
|
});
|
||||||
6
src/config/jwt.config.ts
Normal file
6
src/config/jwt.config.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs('jwt', () => ({
|
||||||
|
secretKey: process.env.JWT_SECRET_KEY || 'rzxlszyykpbgqcflzxsqcysyhljt',
|
||||||
|
expiresIn: process.env.JWT_EXPIRES_IN || '3600s',
|
||||||
|
}));
|
||||||
22
src/config/mongodb.config.spec.ts
Normal file
22
src/config/mongodb.config.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { ConfigModule, ConfigType } from '@nestjs/config';
|
||||||
|
import { TestingModule, Test } from '@nestjs/testing';
|
||||||
|
import mongodbConfig from './mongodb.config';
|
||||||
|
|
||||||
|
describe('mongodbConfig', () => {
|
||||||
|
let config: ConfigType<typeof mongodbConfig>;
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [ConfigModule.forFeature(mongodbConfig)],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
config = module.get<ConfigType<typeof mongodbConfig>>(mongodbConfig.KEY);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(mongodbConfig).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contains uri key', async () => {
|
||||||
|
expect(config.uri).toBe('mongodb://localhost/blog');
|
||||||
|
});
|
||||||
|
});
|
||||||
5
src/config/mongodb.config.ts
Normal file
5
src/config/mongodb.config.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs('mongodb', () => ({
|
||||||
|
uri: process.env.MONGODB_URI || 'mongodb://localhost/blog',
|
||||||
|
}));
|
||||||
@ -1,8 +1,10 @@
|
|||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { databaseProviders } from './database.providers';
|
import { databaseProviders } from './database.providers';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import mongodbConfig from '../config/mongodb.config';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [ConfigModule.forFeature(mongodbConfig)],
|
||||||
providers: [...databaseProviders],
|
providers: [...databaseProviders],
|
||||||
exports: [...databaseProviders],
|
exports: [...databaseProviders],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -9,15 +9,20 @@ import {
|
|||||||
COMMENT_MODEL,
|
COMMENT_MODEL,
|
||||||
} from './database.constants';
|
} from './database.constants';
|
||||||
import { CommentSchema } from './comment.model';
|
import { CommentSchema } from './comment.model';
|
||||||
|
import mongodbConfig from '../config/mongodb.config';
|
||||||
|
import { ConfigType } from '@nestjs/config';
|
||||||
|
|
||||||
export const databaseProviders = [
|
export const databaseProviders = [
|
||||||
{
|
{
|
||||||
provide: DATABASE_CONNECTION,
|
provide: DATABASE_CONNECTION,
|
||||||
useFactory: (): Promise<typeof mongoose> =>
|
useFactory: (
|
||||||
connect('mongodb://localhost/blog', {
|
dbConfig: ConfigType<typeof mongodbConfig>,
|
||||||
|
): Promise<typeof mongoose> =>
|
||||||
|
connect(dbConfig.uri, {
|
||||||
useNewUrlParser: true,
|
useNewUrlParser: true,
|
||||||
useUnifiedTopology: true,
|
useUnifiedTopology: true,
|
||||||
}),
|
}),
|
||||||
|
inject: [mongodbConfig.KEY],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: POST_MODEL,
|
provide: POST_MODEL,
|
||||||
|
|||||||
@ -6,6 +6,9 @@ async function bootstrap() {
|
|||||||
|
|
||||||
// enable shutdown hooks explicitly.
|
// enable shutdown hooks explicitly.
|
||||||
app.enableShutdownHooks();
|
app.enableShutdownHooks();
|
||||||
|
|
||||||
|
app.enableCors();
|
||||||
|
//app.useLogger();
|
||||||
await app.listen(3000);
|
await app.listen(3000);
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|||||||
@ -2,12 +2,12 @@ import {
|
|||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
OnModuleDestroy,
|
OnModuleDestroy,
|
||||||
OnModuleInit,
|
OnModuleInit
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Model } from 'mongoose';
|
import { Model } from 'mongoose';
|
||||||
import { Post } from 'src/database/post.model';
|
|
||||||
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';
|
||||||
|
import { Post } from '../database/post.model';
|
||||||
import { CreatePostDto } from './create-post.dto';
|
import { CreatePostDto } from './create-post.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
@ -35,8 +35,8 @@ describe('Post Controller', () => {
|
|||||||
expect(posts.length).toBe(3);
|
expect(posts.length).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('GET on /posts/:id should return one post ', done => {
|
it('GET on /posts/:id should return one post ', (done) => {
|
||||||
controller.getPostById('1').subscribe(data => {
|
controller.getPostById('1').subscribe((data) => {
|
||||||
expect(data._id).toEqual('1');
|
expect(data._id).toEqual('1');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@ -51,24 +51,41 @@ describe('Post Controller', () => {
|
|||||||
expect(saved.title).toEqual('test title');
|
expect(saved.title).toEqual('test title');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('PUT on /posts/:id should update the existing post', done => {
|
it('PUT on /posts/:id should update the existing post', (done) => {
|
||||||
const post: UpdatePostDto = {
|
const post: UpdatePostDto = {
|
||||||
title: 'test title',
|
title: 'test title',
|
||||||
content: 'test content',
|
content: 'test content',
|
||||||
};
|
};
|
||||||
controller.updatePost('1', post).subscribe(data => {
|
controller.updatePost('1', post).subscribe((data) => {
|
||||||
expect(data.title).toEqual('test title');
|
expect(data.title).toEqual('test title');
|
||||||
expect(data.content).toEqual('test content');
|
expect(data.content).toEqual('test content');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('DELETE on /posts/:id should delete post', done => {
|
it('DELETE on /posts/:id should delete post', (done) => {
|
||||||
controller.deletePostById('1').subscribe(data => {
|
controller.deletePostById('1').subscribe((data) => {
|
||||||
expect(data).toBeTruthy();
|
expect(data).toBeTruthy();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('POST on /posts/:id/comments', async () => {
|
||||||
|
const result = await controller
|
||||||
|
.createCommentForPost('testpost', { content: 'testcomment' })
|
||||||
|
.toPromise();
|
||||||
|
|
||||||
|
expect(result.content).toBe('testcomment');
|
||||||
|
expect(result.post._id).toBe('testpost');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET on /posts/:id/comments', async () => {
|
||||||
|
const result = await controller
|
||||||
|
.getAllCommentsOfPost('testpost')
|
||||||
|
.toPromise();
|
||||||
|
|
||||||
|
expect(result.length).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Replace PostService in provider(useValue: fake object)', () => {
|
describe('Replace PostService in provider(useValue: fake object)', () => {
|
||||||
|
|||||||
@ -3,12 +3,13 @@ import { Test, TestingModule } from '@nestjs/testing';
|
|||||||
import { FilterQuery, Model } from 'mongoose';
|
import { FilterQuery, Model } from 'mongoose';
|
||||||
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', () => {
|
||||||
let service: PostService;
|
let service: PostService;
|
||||||
let model: Model<Post>;
|
let model: Model<Post>;
|
||||||
|
let commentModel: Model<Comment>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
@ -48,7 +49,7 @@ describe('PostService', () => {
|
|||||||
provide: REQUEST,
|
provide: REQUEST,
|
||||||
useValue: {
|
useValue: {
|
||||||
user: {
|
user: {
|
||||||
_id: 'dummnyId',
|
id: 'dummyId',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -57,6 +58,7 @@ describe('PostService', () => {
|
|||||||
|
|
||||||
service = await module.resolve<PostService>(PostService);
|
service = await module.resolve<PostService>(PostService);
|
||||||
model = module.get<Model<Post>>(POST_MODEL);
|
model = module.get<Model<Post>>(POST_MODEL);
|
||||||
|
commentModel = module.get<Model<Comment>>(COMMENT_MODEL);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
@ -117,7 +119,7 @@ describe('PostService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('findById with an existing id should return one post', done => {
|
it('findById with an existing id should return one post', (done) => {
|
||||||
const found = {
|
const found = {
|
||||||
_id: '5ee49c3115a4e75254bb732e',
|
_id: '5ee49c3115a4e75254bb732e',
|
||||||
title: 'Generate a NestJS project',
|
title: 'Generate a NestJS project',
|
||||||
@ -129,11 +131,11 @@ describe('PostService', () => {
|
|||||||
} 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(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -156,13 +158,13 @@ describe('PostService', () => {
|
|||||||
expect(model.create).toBeCalledWith({
|
expect(model.create).toBeCalledWith({
|
||||||
...toCreated,
|
...toCreated,
|
||||||
createdBy: {
|
createdBy: {
|
||||||
_id: 'dummnyId',
|
_id: 'dummyId',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(model.create).toBeCalledTimes(1);
|
expect(model.create).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update post', done => {
|
it('should update post', (done) => {
|
||||||
const toUpdated = {
|
const toUpdated = {
|
||||||
_id: '5ee49c3115a4e75254bb732e',
|
_id: '5ee49c3115a4e75254bb732e',
|
||||||
title: 'test title',
|
title: 'test title',
|
||||||
@ -174,15 +176,15 @@ describe('PostService', () => {
|
|||||||
} 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._id).toBe('5ee49c3115a4e75254bb732e');
|
||||||
},
|
},
|
||||||
error: error => console.log(error),
|
error: (error) => console.log(error),
|
||||||
complete: done(),
|
complete: done(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete post', done => {
|
it('should delete post', (done) => {
|
||||||
const toDeleted = {
|
const toDeleted = {
|
||||||
_id: '5ee49c3115a4e75254bb732e',
|
_id: '5ee49c3115a4e75254bb732e',
|
||||||
title: 'test title',
|
title: 'test title',
|
||||||
@ -193,13 +195,13 @@ describe('PostService', () => {
|
|||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
service.deleteById('anystring').subscribe({
|
service.deleteById('anystring').subscribe({
|
||||||
next: data => expect(data._id).toEqual('5ee49c3115a4e75254bb732e'),
|
next: (data) => expect(data._id).toEqual('5ee49c3115a4e75254bb732e'),
|
||||||
error: error => console.log(error),
|
error: (error) => console.log(error),
|
||||||
complete: done(),
|
complete: done(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete all post', done => {
|
it('should delete all post', (done) => {
|
||||||
jest.spyOn(model, 'deleteMany').mockReturnValue({
|
jest.spyOn(model, 'deleteMany').mockReturnValue({
|
||||||
exec: jest.fn().mockResolvedValueOnce({
|
exec: jest.fn().mockResolvedValueOnce({
|
||||||
deletedCount: 1,
|
deletedCount: 1,
|
||||||
@ -207,9 +209,57 @@ describe('PostService', () => {
|
|||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
service.deleteAll().subscribe({
|
service.deleteAll().subscribe({
|
||||||
next: data => expect(data).toBeTruthy,
|
next: (data) => expect(data).toBeTruthy,
|
||||||
error: error => console.log(error),
|
error: (error) => console.log(error),
|
||||||
complete: done(),
|
complete: done(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should create comment ', async () => {
|
||||||
|
const comment = { content: 'test' };
|
||||||
|
jest.spyOn(commentModel, 'create').mockResolvedValue({
|
||||||
|
...comment,
|
||||||
|
post: { _id: 'test' },
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const result = await service.createCommentFor('test', comment).toPromise();
|
||||||
|
expect(result.content).toEqual('test');
|
||||||
|
expect(commentModel.create).toBeCalledWith({
|
||||||
|
...comment,
|
||||||
|
post: { _id: 'test' },
|
||||||
|
createdBy: { _id: 'dummyId' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get comments of post ', async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(commentModel, 'find')
|
||||||
|
.mockImplementation(
|
||||||
|
(
|
||||||
|
conditions: FilterQuery<Comment>,
|
||||||
|
callback?: (err: any, res: Comment[]) => void,
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
select: jest
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue({
|
||||||
|
exec: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue([
|
||||||
|
{
|
||||||
|
_id: 'test',
|
||||||
|
content: 'content',
|
||||||
|
post: { _id: '_test_id' },
|
||||||
|
}
|
||||||
|
] as any),
|
||||||
|
}),
|
||||||
|
} as any;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await service.commentsOf('test').toPromise();
|
||||||
|
expect(result.length).toBe(1);
|
||||||
|
expect(result[0].content).toEqual('content');
|
||||||
|
expect(commentModel.find).toBeCalledWith({ post: { _id: 'test' } });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -10,8 +10,7 @@ import { Comment } from '../database/comment.model';
|
|||||||
// use `Pick<T, key of T>` instead of writing an extra interface.
|
// use `Pick<T, key of T>` instead of writing an extra interface.
|
||||||
// see: https://dev.to/jonrimmer/typesafe-mocking-in-typescript-3b50
|
// see: https://dev.to/jonrimmer/typesafe-mocking-in-typescript-3b50
|
||||||
// also see: https://www.typescriptlang.org/docs/handbook/utility-types.html#picktk
|
// also see: https://www.typescriptlang.org/docs/handbook/utility-types.html#picktk
|
||||||
export class PostServiceStub implements Pick<PostService, keyof PostService>{
|
export class PostServiceStub implements Pick<PostService, keyof PostService> {
|
||||||
|
|
||||||
private posts: Post[] = [
|
private posts: Post[] = [
|
||||||
{
|
{
|
||||||
_id: '5ee49c3115a4e75254bb732e',
|
_id: '5ee49c3115a4e75254bb732e',
|
||||||
@ -22,12 +21,19 @@ export class PostServiceStub implements Pick<PostService, keyof PostService>{
|
|||||||
_id: '5ee49c3115a4e75254bb732f',
|
_id: '5ee49c3115a4e75254bb732f',
|
||||||
title: 'Create CRUD RESTful APIs',
|
title: 'Create CRUD RESTful APIs',
|
||||||
content: 'content',
|
content: 'content',
|
||||||
} as Post,
|
} as Post,
|
||||||
{
|
{
|
||||||
_id: '5ee49c3115a4e75254bb7330',
|
_id: '5ee49c3115a4e75254bb7330',
|
||||||
title: 'Connect to MongoDB',
|
title: 'Connect to MongoDB',
|
||||||
content: 'content',
|
content: 'content',
|
||||||
} as Post,
|
} as Post,
|
||||||
|
];
|
||||||
|
|
||||||
|
private comments: Comment[] = [
|
||||||
|
{
|
||||||
|
post: { _id: '5ee49c3115a4e75254bb732e' },
|
||||||
|
content: 'comment of post',
|
||||||
|
} as Comment,
|
||||||
];
|
];
|
||||||
|
|
||||||
findAll(): Observable<Post[]> {
|
findAll(): Observable<Post[]> {
|
||||||
@ -39,31 +45,30 @@ export class PostServiceStub implements Pick<PostService, keyof PostService>{
|
|||||||
return of({ _id: id, title, content } as Post);
|
return of({ _id: id, title, content } as Post);
|
||||||
}
|
}
|
||||||
|
|
||||||
save(data: CreatePostDto) : Observable<Post>{
|
save(data: CreatePostDto): Observable<Post> {
|
||||||
return of({ _id: this.posts[0]._id, ...data } as Post);
|
return of({ _id: this.posts[0]._id, ...data } as Post);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(id: string, data: UpdatePostDto) : Observable<Post>{
|
update(id: string, data: UpdatePostDto): Observable<Post> {
|
||||||
return of({ _id: id, ...data } as Post);
|
return of({ _id: id, ...data } as Post);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteById(id: string) : Observable<Post>{
|
deleteById(id: string): Observable<Post> {
|
||||||
return of({ ...this.posts[0], _id: id } as Post);
|
return of({ ...this.posts[0], _id: id } as Post);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteAll(): Observable<any> {
|
deleteAll(): Observable<any> {
|
||||||
throw new Error("Method not implemented.");
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
createCommentFor(
|
createCommentFor(
|
||||||
id: string,
|
postid: string,
|
||||||
data: CreateCommentDto,
|
data: CreateCommentDto,
|
||||||
): Observable<Comment> {
|
): Observable<Comment> {
|
||||||
throw new Error('Method not implemented.');
|
return of({ id: 'test', post: { _id: postid }, ...data } as Comment);
|
||||||
}
|
}
|
||||||
commentsOf(
|
|
||||||
id: string,
|
commentsOf(id: string): Observable<Comment[]> {
|
||||||
): Observable<Comment[]> {
|
return of(this.comments);
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Inject, Injectable, Scope } 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 } from 'rxjs';
|
||||||
import { AuthenticatedRequest } from 'src/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';
|
||||||
import { Post } from '../database/post.model';
|
import { Post } from '../database/post.model';
|
||||||
@ -47,7 +47,7 @@ export class PostService {
|
|||||||
//console.log('req.user:'+JSON.stringify(this.req.user));
|
//console.log('req.user:'+JSON.stringify(this.req.user));
|
||||||
const createPost = this.postModel.create({
|
const createPost = this.postModel.create({
|
||||||
...data,
|
...data,
|
||||||
createdBy: { _id: this.req.user._id },
|
createdBy: { _id: this.req.user.id },
|
||||||
});
|
});
|
||||||
return from(createPost);
|
return from(createPost);
|
||||||
}
|
}
|
||||||
@ -57,7 +57,7 @@ export class PostService {
|
|||||||
this.postModel
|
this.postModel
|
||||||
.findOneAndUpdate(
|
.findOneAndUpdate(
|
||||||
{ _id: id },
|
{ _id: id },
|
||||||
{ ...data, updatedBy: { _id: this.req.user._id } },
|
{ ...data, updatedBy: { _id: this.req.user.id } },
|
||||||
)
|
)
|
||||||
.exec(),
|
.exec(),
|
||||||
);
|
);
|
||||||
@ -76,7 +76,7 @@ export class PostService {
|
|||||||
const createdComment = this.commentModel.create({
|
const createdComment = this.commentModel.create({
|
||||||
post: { _id: id },
|
post: { _id: id },
|
||||||
...data,
|
...data,
|
||||||
createdBy: { _id: this.req.user._id },
|
createdBy: { _id: this.req.user.id },
|
||||||
});
|
});
|
||||||
return from(createdComment);
|
return from(createdComment);
|
||||||
}
|
}
|
||||||
|
|||||||
23
src/user/profile.controller.spec.ts
Normal file
23
src/user/profile.controller.spec.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { ProfileController } from './profile.controller';
|
||||||
|
import { AuthenticatedRequest } from '../auth/authenticated-request.interface';
|
||||||
|
|
||||||
|
describe('ProfileController', () => {
|
||||||
|
let controller: ProfileController;
|
||||||
|
beforeEach(async () => {
|
||||||
|
const app: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [ProfileController],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = app.get<ProfileController>(ProfileController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call req', async () => {
|
||||||
|
const req = {user :{username:'test'}} as AuthenticatedRequest;
|
||||||
|
expect(controller.getProfile(req).username).toBe('test');
|
||||||
|
});
|
||||||
|
});
|
||||||
13
src/user/profile.controller.ts
Normal file
13
src/user/profile.controller.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Controller, Get, Req, UseGuards } from '@nestjs/common';
|
||||||
|
import { Request } from 'express';
|
||||||
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class ProfileController {
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Get('profile')
|
||||||
|
getProfile(@Req() req: Request): any {
|
||||||
|
return req.user;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/user/user.dto.ts
Normal file
13
src/user/user.dto.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { RoleType } from '../database/role-type.enum';
|
||||||
|
|
||||||
|
export class UserDto {
|
||||||
|
readonly id: string;
|
||||||
|
readonly username: string;
|
||||||
|
readonly email: string;
|
||||||
|
readonly password: string;
|
||||||
|
readonly firstName?: string;
|
||||||
|
readonly lastName?: string;
|
||||||
|
readonly roles?: RoleType[];
|
||||||
|
readonly createdAt?: Date;
|
||||||
|
readonly updatedAt?: Date;
|
||||||
|
}
|
||||||
@ -1,13 +1,12 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { UserService } from './user.service';
|
import { DatabaseModule } from '../database/database.module';
|
||||||
|
import { ProfileController } from './profile.controller';
|
||||||
import { UserDataInitializerService } from './user-data-initializer.service';
|
import { UserDataInitializerService } from './user-data-initializer.service';
|
||||||
import { DatabaseModule } from 'src/database/database.module';
|
import { UserService } from './user.service';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [DatabaseModule],
|
imports: [DatabaseModule],
|
||||||
providers: [
|
providers: [UserService, UserDataInitializerService],
|
||||||
UserService,
|
|
||||||
UserDataInitializerService,
|
|
||||||
],
|
|
||||||
exports: [UserService],
|
exports: [UserService],
|
||||||
|
controllers: [ProfileController],
|
||||||
})
|
})
|
||||||
export class UserModule {}
|
export class UserModule {}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts", "**/*stub.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
"target": "es2017",
|
"target": "es2017",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"baseUrl": "./",
|
"baseUrl": "./src",
|
||||||
"incremental": true
|
"incremental": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user