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==",
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
||||
@ -2192,6 +2198,18 @@
|
||||
"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": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-7.3.1.tgz",
|
||||
@ -5182,6 +5200,16 @@
|
||||
"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": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
|
||||
@ -10370,6 +10398,11 @@
|
||||
"integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=",
|
||||
"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": {
|
||||
"version": "4.3.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^7.2.0",
|
||||
"@nestjs/config": "^0.5.0",
|
||||
"@nestjs/core": "^7.2.0",
|
||||
"@nestjs/jwt": "^7.0.0",
|
||||
"@nestjs/passport": "^7.1.0",
|
||||
@ -37,6 +38,7 @@
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^9.0.1",
|
||||
"@commitlint/config-conventional": "^9.0.1",
|
||||
"@golevelup/nestjs-testing": "^0.1.2",
|
||||
"@nestjs/cli": "^7.4.1",
|
||||
"@nestjs/schematics": "^7.0.0",
|
||||
"@nestjs/testing": "^7.2.0",
|
||||
@ -46,8 +48,8 @@
|
||||
"@types/node": "^14.0.14",
|
||||
"@types/passport-jwt": "^3.0.3",
|
||||
"@types/passport-local": "^1.0.33",
|
||||
"@typescript-eslint/eslint-plugin": "3.5.0",
|
||||
"@types/supertest": "^2.0.10",
|
||||
"@typescript-eslint/eslint-plugin": "3.5.0",
|
||||
"@typescript-eslint/parser": "3.5.0",
|
||||
"eslint": "7.3.1",
|
||||
"eslint-config-prettier": "^6.10.0",
|
||||
|
||||
@ -1,37 +1,17 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppController } from './app.controller';
|
||||
import { AuthService } from './auth/auth.service';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
describe('AppController', () => {
|
||||
let appController: AppController;
|
||||
let authService: AuthService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
{
|
||||
provide: AuthService,
|
||||
useValue: {
|
||||
constructor: jest.fn(),
|
||||
login: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
appController = app.get<AppController>(AppController);
|
||||
authService = app.get<AuthService>(AuthService);
|
||||
});
|
||||
|
||||
describe('root', () => {
|
||||
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();
|
||||
});
|
||||
it('should be defined', () => {
|
||||
expect(appController).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,24 +1,6 @@
|
||||
import { Controller, Post, UseGuards, Get, Req } 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';
|
||||
import { Controller } from '@nestjs/common';
|
||||
|
||||
@Controller()
|
||||
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 { ConfigModule } from '@nestjs/config';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
@ -8,6 +9,7 @@ import { UserModule } from './user/user.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({ ignoreEnvFile: true }),
|
||||
DatabaseModule,
|
||||
PostModule,
|
||||
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';
|
||||
|
||||
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 { PassportModule } from '@nestjs/passport';
|
||||
import { LocalStrategy } from './local.strategy';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { jwtConstants } from './auth.constants';
|
||||
import { JwtModule, JwtModuleOptions } from '@nestjs/jwt';
|
||||
import { JwtStrategy } from './jwt.strategy';
|
||||
import { ConfigModule, ConfigType } from '@nestjs/config';
|
||||
import jwtConfig from '../config/jwt.config';
|
||||
import { AuthController } from './auth.controller';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forFeature(jwtConfig),
|
||||
UserModule,
|
||||
PassportModule.register({ defaultStrategy: 'jwt' }),
|
||||
JwtModule.register({
|
||||
secret: jwtConstants.secret,
|
||||
signOptions: { expiresIn: '3600s' },
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule.forFeature(jwtConfig)],
|
||||
useFactory: (config: ConfigType<typeof jwtConfig>) => {
|
||||
return {
|
||||
secret: config.secretKey,
|
||||
signOptions: { expiresIn: config.expiresIn },
|
||||
} as JwtModuleOptions;
|
||||
},
|
||||
inject: [jwtConfig.KEY],
|
||||
}),
|
||||
],
|
||||
providers: [AuthService, LocalStrategy, JwtStrategy],
|
||||
exports: [AuthService],
|
||||
controllers: [AuthController],
|
||||
})
|
||||
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 { 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', () => {
|
||||
let service: AuthService;
|
||||
let userService: UserService;
|
||||
let jwtService: JwtService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@ -28,9 +34,108 @@ describe('AuthService', () => {
|
||||
}).compile();
|
||||
|
||||
service = module.get<AuthService>(AuthService);
|
||||
userService = module.get<UserService>(UserService);
|
||||
jwtService = module.get<JwtService>(JwtService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
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 { map, flatMap } from 'rxjs/operators';
|
||||
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()
|
||||
export class AuthService {
|
||||
@ -11,13 +14,13 @@ export class AuthService {
|
||||
private jwtService: JwtService,
|
||||
) {}
|
||||
|
||||
validateUser(username: string, pass: string): Observable<any> {
|
||||
validateUser(username: string, pass: string): Observable<UserPrincipal> {
|
||||
return this.userService.findByUsername(username).pipe(
|
||||
flatMap(user => {
|
||||
flatMap((user) => {
|
||||
//console.log('userService.findByUsername::' + JSON.stringify(user));
|
||||
if (user && user.password === pass) {
|
||||
const { _id, username, email, roles } = user;
|
||||
return of({ _id, username, email, roles });
|
||||
return of({ id: _id, username, email, roles });
|
||||
}
|
||||
return EMPTY;
|
||||
}),
|
||||
@ -25,20 +28,21 @@ export class AuthService {
|
||||
}
|
||||
|
||||
// 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> {
|
||||
console.log(user);
|
||||
const payload = {
|
||||
login(user: UserPrincipal): Observable<AccessToken> {
|
||||
//console.log(user);
|
||||
const payload: JwtPayload = {
|
||||
upn: user.username, //upn is defined in Microprofile JWT spec, a human readable principal name.
|
||||
sub: user._id,
|
||||
sub: user.id,
|
||||
email: user.email,
|
||||
roles: user.roles,
|
||||
};
|
||||
return from(this.jwtService.signAsync(payload)).pipe(
|
||||
map(access_token => {
|
||||
map((access_token) => {
|
||||
return { access_token };
|
||||
}),
|
||||
);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { User } from 'src/database/user.model';
|
||||
import { Request } from 'express';
|
||||
import { UserPrincipal } from './user-principal.interface';
|
||||
|
||||
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';
|
||||
|
||||
describe('JwtAuthGuard', () => {
|
||||
describe('LocalAuthGuard', () => {
|
||||
let guard: JwtAuthGuard;
|
||||
beforeEach(() => {
|
||||
guard = new JwtAuthGuard();
|
||||
});
|
||||
|
||||
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 { 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()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor() {
|
||||
constructor(@Inject(jwtConfig.KEY) config: ConfigType<typeof jwtConfig>) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: jwtConstants.secret,
|
||||
secretOrKey: config.secretKey,
|
||||
});
|
||||
}
|
||||
|
||||
//payload is the decoded jwt clmais.
|
||||
validate(payload: any): any {
|
||||
validate(payload: JwtPayload): UserPrincipal {
|
||||
//console.log('jwt payload:' + JSON.stringify(payload));
|
||||
return {
|
||||
username: payload.upn,
|
||||
email: payload.email,
|
||||
_id: payload.sub,
|
||||
id: payload.sub,
|
||||
roles: payload.roles,
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,21 @@
|
||||
import { ExecutionContext } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { LocalAuthGuard } from './local-auth.guard';
|
||||
|
||||
describe('LocalAuthGuard', () => {
|
||||
it('should be defined', () => {
|
||||
expect(new LocalAuthGuard()).toBeDefined();
|
||||
let guard: LocalAuthGuard;
|
||||
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 { PassportStrategy } from '@nestjs/passport';
|
||||
import { Strategy } from 'passport-local';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, throwIfEmpty } from 'rxjs/operators';
|
||||
import { AuthService } from './auth.service';
|
||||
import { UserPrincipal } from './user-principal.interface';
|
||||
|
||||
@Injectable()
|
||||
export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||
@ -30,8 +29,8 @@ export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||
// .pipe(throwIfEmpty(() => new UnauthorizedException()));
|
||||
// }
|
||||
|
||||
async validate(username: string, password: string): Promise<any> {
|
||||
const user = await this.authService
|
||||
async validate(username: string, password: string): Promise<UserPrincipal> {
|
||||
const user: UserPrincipal = await this.authService
|
||||
.validateUser(username, password)
|
||||
.toPromise();
|
||||
|
||||
|
||||
@ -1,7 +1,85 @@
|
||||
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', () => {
|
||||
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', () => {
|
||||
//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 { databaseProviders } from './database.providers';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import mongodbConfig from '../config/mongodb.config';
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule.forFeature(mongodbConfig)],
|
||||
providers: [...databaseProviders],
|
||||
exports: [...databaseProviders],
|
||||
})
|
||||
|
||||
@ -9,15 +9,20 @@ import {
|
||||
COMMENT_MODEL,
|
||||
} from './database.constants';
|
||||
import { CommentSchema } from './comment.model';
|
||||
import mongodbConfig from '../config/mongodb.config';
|
||||
import { ConfigType } from '@nestjs/config';
|
||||
|
||||
export const databaseProviders = [
|
||||
{
|
||||
provide: DATABASE_CONNECTION,
|
||||
useFactory: (): Promise<typeof mongoose> =>
|
||||
connect('mongodb://localhost/blog', {
|
||||
useFactory: (
|
||||
dbConfig: ConfigType<typeof mongodbConfig>,
|
||||
): Promise<typeof mongoose> =>
|
||||
connect(dbConfig.uri, {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
}),
|
||||
inject: [mongodbConfig.KEY],
|
||||
},
|
||||
{
|
||||
provide: POST_MODEL,
|
||||
|
||||
@ -6,6 +6,9 @@ async function bootstrap() {
|
||||
|
||||
// enable shutdown hooks explicitly.
|
||||
app.enableShutdownHooks();
|
||||
|
||||
app.enableCors();
|
||||
//app.useLogger();
|
||||
await app.listen(3000);
|
||||
}
|
||||
bootstrap();
|
||||
|
||||
@ -2,12 +2,12 @@ import {
|
||||
Inject,
|
||||
Injectable,
|
||||
OnModuleDestroy,
|
||||
OnModuleInit,
|
||||
OnModuleInit
|
||||
} from '@nestjs/common';
|
||||
import { Model } from 'mongoose';
|
||||
import { Post } from 'src/database/post.model';
|
||||
import { Comment } from '../database/comment.model';
|
||||
import { COMMENT_MODEL, POST_MODEL } from '../database/database.constants';
|
||||
import { Post } from '../database/post.model';
|
||||
import { CreatePostDto } from './create-post.dto';
|
||||
|
||||
@Injectable()
|
||||
|
||||
@ -35,8 +35,8 @@ describe('Post Controller', () => {
|
||||
expect(posts.length).toBe(3);
|
||||
});
|
||||
|
||||
it('GET on /posts/:id should return one post ', done => {
|
||||
controller.getPostById('1').subscribe(data => {
|
||||
it('GET on /posts/:id should return one post ', (done) => {
|
||||
controller.getPostById('1').subscribe((data) => {
|
||||
expect(data._id).toEqual('1');
|
||||
done();
|
||||
});
|
||||
@ -51,24 +51,41 @@ describe('Post Controller', () => {
|
||||
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 = {
|
||||
title: 'test title',
|
||||
content: 'test content',
|
||||
};
|
||||
controller.updatePost('1', post).subscribe(data => {
|
||||
controller.updatePost('1', post).subscribe((data) => {
|
||||
expect(data.title).toEqual('test title');
|
||||
expect(data.content).toEqual('test content');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('DELETE on /posts/:id should delete post', done => {
|
||||
controller.deletePostById('1').subscribe(data => {
|
||||
it('DELETE on /posts/:id should delete post', (done) => {
|
||||
controller.deletePostById('1').subscribe((data) => {
|
||||
expect(data).toBeTruthy();
|
||||
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)', () => {
|
||||
|
||||
@ -3,12 +3,13 @@ import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { FilterQuery, Model } from 'mongoose';
|
||||
import { COMMENT_MODEL, POST_MODEL } from '../database/database.constants';
|
||||
import { Post } from '../database/post.model';
|
||||
import { Comment } from '../database/comment.model';
|
||||
import { PostService } from './post.service';
|
||||
|
||||
|
||||
describe('PostService', () => {
|
||||
let service: PostService;
|
||||
let model: Model<Post>;
|
||||
let commentModel: Model<Comment>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@ -48,7 +49,7 @@ describe('PostService', () => {
|
||||
provide: REQUEST,
|
||||
useValue: {
|
||||
user: {
|
||||
_id: 'dummnyId',
|
||||
id: 'dummyId',
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -57,6 +58,7 @@ describe('PostService', () => {
|
||||
|
||||
service = await module.resolve<PostService>(PostService);
|
||||
model = module.get<Model<Post>>(POST_MODEL);
|
||||
commentModel = module.get<Model<Comment>>(COMMENT_MODEL);
|
||||
});
|
||||
|
||||
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 = {
|
||||
_id: '5ee49c3115a4e75254bb732e',
|
||||
title: 'Generate a NestJS project',
|
||||
@ -129,11 +131,11 @@ describe('PostService', () => {
|
||||
} as any);
|
||||
|
||||
service.findById('1').subscribe({
|
||||
next: data => {
|
||||
next: (data) => {
|
||||
expect(data._id).toBe('5ee49c3115a4e75254bb732e');
|
||||
expect(data.title).toEqual('Generate a NestJS project');
|
||||
},
|
||||
error: error => console.log(error),
|
||||
error: (error) => console.log(error),
|
||||
complete: done(),
|
||||
});
|
||||
});
|
||||
@ -156,13 +158,13 @@ describe('PostService', () => {
|
||||
expect(model.create).toBeCalledWith({
|
||||
...toCreated,
|
||||
createdBy: {
|
||||
_id: 'dummnyId',
|
||||
_id: 'dummyId',
|
||||
},
|
||||
});
|
||||
expect(model.create).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should update post', done => {
|
||||
it('should update post', (done) => {
|
||||
const toUpdated = {
|
||||
_id: '5ee49c3115a4e75254bb732e',
|
||||
title: 'test title',
|
||||
@ -174,15 +176,15 @@ describe('PostService', () => {
|
||||
} as any);
|
||||
|
||||
service.update('5ee49c3115a4e75254bb732e', toUpdated).subscribe({
|
||||
next: data => {
|
||||
next: (data) => {
|
||||
expect(data._id).toBe('5ee49c3115a4e75254bb732e');
|
||||
},
|
||||
error: error => console.log(error),
|
||||
error: (error) => console.log(error),
|
||||
complete: done(),
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete post', done => {
|
||||
it('should delete post', (done) => {
|
||||
const toDeleted = {
|
||||
_id: '5ee49c3115a4e75254bb732e',
|
||||
title: 'test title',
|
||||
@ -193,13 +195,13 @@ describe('PostService', () => {
|
||||
} as any);
|
||||
|
||||
service.deleteById('anystring').subscribe({
|
||||
next: data => expect(data._id).toEqual('5ee49c3115a4e75254bb732e'),
|
||||
error: error => console.log(error),
|
||||
next: (data) => expect(data._id).toEqual('5ee49c3115a4e75254bb732e'),
|
||||
error: (error) => console.log(error),
|
||||
complete: done(),
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete all post', done => {
|
||||
it('should delete all post', (done) => {
|
||||
jest.spyOn(model, 'deleteMany').mockReturnValue({
|
||||
exec: jest.fn().mockResolvedValueOnce({
|
||||
deletedCount: 1,
|
||||
@ -207,9 +209,57 @@ describe('PostService', () => {
|
||||
} as any);
|
||||
|
||||
service.deleteAll().subscribe({
|
||||
next: data => expect(data).toBeTruthy,
|
||||
error: error => console.log(error),
|
||||
next: (data) => expect(data).toBeTruthy,
|
||||
error: (error) => console.log(error),
|
||||
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.
|
||||
// see: https://dev.to/jonrimmer/typesafe-mocking-in-typescript-3b50
|
||||
// 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[] = [
|
||||
{
|
||||
_id: '5ee49c3115a4e75254bb732e',
|
||||
@ -22,12 +21,19 @@ export class PostServiceStub implements Pick<PostService, keyof PostService>{
|
||||
_id: '5ee49c3115a4e75254bb732f',
|
||||
title: 'Create CRUD RESTful APIs',
|
||||
content: 'content',
|
||||
} as Post,
|
||||
} as Post,
|
||||
{
|
||||
_id: '5ee49c3115a4e75254bb7330',
|
||||
title: 'Connect to MongoDB',
|
||||
content: 'content',
|
||||
} as Post,
|
||||
} as Post,
|
||||
];
|
||||
|
||||
private comments: Comment[] = [
|
||||
{
|
||||
post: { _id: '5ee49c3115a4e75254bb732e' },
|
||||
content: 'comment of post',
|
||||
} as Comment,
|
||||
];
|
||||
|
||||
findAll(): Observable<Post[]> {
|
||||
@ -39,31 +45,30 @@ export class PostServiceStub implements Pick<PostService, keyof PostService>{
|
||||
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);
|
||||
}
|
||||
|
||||
update(id: string, data: UpdatePostDto) : Observable<Post>{
|
||||
update(id: string, data: UpdatePostDto): Observable<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);
|
||||
}
|
||||
|
||||
deleteAll(): Observable<any> {
|
||||
throw new Error("Method not implemented.");
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
createCommentFor(
|
||||
id: string,
|
||||
postid: string,
|
||||
data: CreateCommentDto,
|
||||
): Observable<Comment> {
|
||||
throw new Error('Method not implemented.');
|
||||
return of({ id: 'test', post: { _id: postid }, ...data } as Comment);
|
||||
}
|
||||
commentsOf(
|
||||
id: string,
|
||||
): Observable<Comment[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
|
||||
commentsOf(id: string): Observable<Comment[]> {
|
||||
return of(this.comments);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { Model } from 'mongoose';
|
||||
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_MODEL, POST_MODEL } from '../database/database.constants';
|
||||
import { Post } from '../database/post.model';
|
||||
@ -47,7 +47,7 @@ export class PostService {
|
||||
//console.log('req.user:'+JSON.stringify(this.req.user));
|
||||
const createPost = this.postModel.create({
|
||||
...data,
|
||||
createdBy: { _id: this.req.user._id },
|
||||
createdBy: { _id: this.req.user.id },
|
||||
});
|
||||
return from(createPost);
|
||||
}
|
||||
@ -57,7 +57,7 @@ export class PostService {
|
||||
this.postModel
|
||||
.findOneAndUpdate(
|
||||
{ _id: id },
|
||||
{ ...data, updatedBy: { _id: this.req.user._id } },
|
||||
{ ...data, updatedBy: { _id: this.req.user.id } },
|
||||
)
|
||||
.exec(),
|
||||
);
|
||||
@ -76,7 +76,7 @@ export class PostService {
|
||||
const createdComment = this.commentModel.create({
|
||||
post: { _id: id },
|
||||
...data,
|
||||
createdBy: { _id: this.req.user._id },
|
||||
createdBy: { _id: this.req.user.id },
|
||||
});
|
||||
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 { UserService } from './user.service';
|
||||
import { DatabaseModule } from '../database/database.module';
|
||||
import { ProfileController } from './profile.controller';
|
||||
import { UserDataInitializerService } from './user-data-initializer.service';
|
||||
import { DatabaseModule } from 'src/database/database.module';
|
||||
import { UserService } from './user.service';
|
||||
@Module({
|
||||
imports: [DatabaseModule],
|
||||
providers: [
|
||||
UserService,
|
||||
UserDataInitializerService,
|
||||
],
|
||||
providers: [UserService, UserDataInitializerService],
|
||||
exports: [UserService],
|
||||
controllers: [ProfileController],
|
||||
})
|
||||
export class UserModule {}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
{
|
||||
"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",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"baseUrl": "./src",
|
||||
"incremental": true
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user