From 8240826598c9686ccd1c6f1da70e834b1eac0755 Mon Sep 17 00:00:00 2001 From: Daehyun Kim <18080546+vimkim@users.noreply.github.com> Date: Sun, 11 Feb 2024 20:53:33 +0900 Subject: [PATCH] =?UTF-8?q?server=20E2E=20test=20=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1=20=EB=B0=8F=20?= =?UTF-8?q?=EB=8F=99=EC=9E=91=20=ED=99=95=EC=9D=B8=20=EC=84=B1=EA=B3=B5=20?= =?UTF-8?q?(#279)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * allow synthetic default import * install dotenv * move all global settings in main.ts to a app.module.ts for better e2e integration test * move passport to auth guard for better modularity * provide global exception filter * provide global validation pipe through injection * refactor: organize redis & session middleware creation * rename custom logger and modules to make more senses * minor: socket import esmoduleinterop * test(!): write a working e2e test for the project for automation! --- server/package-lock.json | 18 ++++- server/package.json | 1 + server/src/app.module.ts | 35 +++++++--- server/src/auth/auth.module.ts | 14 ++-- .../src/common/exception.filter.provider.ts | 9 +++ .../exception.filter.ts} | 4 +- server/src/common/middleware/session.ts | 32 +++++++++ server/src/common/validation.pipe.ts | 15 +++++ .../custom.logger.ts} | 6 +- server/src/logger/logger.module.ts | 8 +++ server/src/main.ts | 66 ++++--------------- server/src/socket/socket.adapter.ts | 4 +- server/test/app.e2e-spec.ts | 35 ++++++++-- server/tsconfig.json | 5 +- 14 files changed, 163 insertions(+), 89 deletions(-) create mode 100644 server/src/common/exception.filter.provider.ts rename server/src/{exceptions/exceptions.filter.ts => common/exception.filter.ts} (89%) create mode 100644 server/src/common/middleware/session.ts create mode 100644 server/src/common/validation.pipe.ts rename server/src/{short-logger/short-logger.service.ts => logger/custom.logger.ts} (85%) create mode 100644 server/src/logger/logger.module.ts diff --git a/server/package-lock.json b/server/package-lock.json index f2bba01..7f0db7f 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -26,6 +26,7 @@ "class-validator": "^0.14.0", "connect-redis": "^7.1.1", "cookie-parser": "^1.4.6", + "dotenv": "^16.4.1", "express-session": "^1.17.3", "ioredis": "^5.3.2", "morgan": "^1.10.0", @@ -1691,6 +1692,17 @@ "reflect-metadata": "^0.1.13" } }, + "node_modules/@nestjs/config/node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, "node_modules/@nestjs/config/node_modules/uuid": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", @@ -4515,9 +4527,9 @@ } }, "node_modules/dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "version": "16.4.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz", + "integrity": "sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==", "engines": { "node": ">=12" }, diff --git a/server/package.json b/server/package.json index a03162a..03ce3b1 100644 --- a/server/package.json +++ b/server/package.json @@ -38,6 +38,7 @@ "class-validator": "^0.14.0", "connect-redis": "^7.1.1", "cookie-parser": "^1.4.6", + "dotenv": "^16.4.1", "express-session": "^1.17.3", "ioredis": "^5.3.2", "morgan": "^1.10.0", diff --git a/server/src/app.module.ts b/server/src/app.module.ts index a835603..6601941 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -1,25 +1,26 @@ -import { Logger, Module } from '@nestjs/common'; +import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { PassportModule } from '@nestjs/passport'; import { TypeOrmModule } from '@nestjs/typeorm'; +import cookieParser from 'cookie-parser'; +import express from 'express'; +import passport from 'passport'; import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AuthModule } from './auth/auth.module'; +import { provideGlobalExceptionFilter } from './common/exception.filter.provider'; +import { globalValidationPipe as provideGlobalValidationPipe } from './common/validation.pipe'; +import { LoggerModule } from './logger/logger.module'; import { ProblemModule } from './problem/problem.module'; import { RoomUserModule } from './room-user/room-user.module'; import { RoomModule } from './room/room.module'; -import { ShortLoggerService } from './short-logger/short-logger.service'; import { SocketModule } from './socket/socket.module'; import { SubmissionModule } from './submission/submission.module'; import { UserModule } from './user/user.module'; @Module({ imports: [ - PassportModule.register({ - session: true, - }), ConfigModule.forRoot({ isGlobal: true, }), @@ -42,10 +43,26 @@ import { UserModule } from './user/user.module'; ProblemModule, SubmissionModule, RoomUserModule, + LoggerModule, ], controllers: [AppController], - providers: [AppService, Logger, ShortLoggerService], + providers: [ + AppService, + provideGlobalValidationPipe(), + provideGlobalExceptionFilter(), + ], }) -export class AppModule { - constructor() {} +export class AppModule implements NestModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(cookieParser()).forRoutes('*'); + + // passport + consumer + .apply(express.json()) + .forRoutes('*') + .apply(passport.initialize()) + .forRoutes('*') + .apply(passport.session()) + .forRoutes('*'); + } } diff --git a/server/src/auth/auth.module.ts b/server/src/auth/auth.module.ts index a4769bf..7f007b4 100644 --- a/server/src/auth/auth.module.ts +++ b/server/src/auth/auth.module.ts @@ -1,26 +1,22 @@ import { Module } from '@nestjs/common'; -import { GithubStrategy, MockStrategy } from './auth.strategy'; +import { PassportModule } from '@nestjs/passport'; +import { UserModule } from '../user/user.module'; import { AuthController } from './auth.controller'; +import { GithubAuthGuard, MockAuthGuard } from './auth.guard'; import { LocalSerializer } from './auth.serializer'; -import { UserModule } from '../user/user.module'; import { AuthService } from './auth.service'; -import { PassportModule } from '@nestjs/passport'; -import { GithubAuthGuard, MockAuthGuard } from './auth.guard'; +import { GithubStrategy, MockStrategy } from './auth.strategy'; @Module({ providers: [ GithubStrategy, GithubAuthGuard, MockAuthGuard, - // MockAuthGuard, - - // SessionAuthGuard, MockStrategy, - AuthService, LocalSerializer, ], controllers: [AuthController], - imports: [UserModule, PassportModule], + imports: [UserModule, PassportModule.register({ session: true })], }) export class AuthModule {} diff --git a/server/src/common/exception.filter.provider.ts b/server/src/common/exception.filter.provider.ts new file mode 100644 index 0000000..1ba58f3 --- /dev/null +++ b/server/src/common/exception.filter.provider.ts @@ -0,0 +1,9 @@ +import { APP_FILTER } from '@nestjs/core'; +import { GlobalExceptionFilter } from 'src/common/exception.filter'; + +export const provideGlobalExceptionFilter = () => { + return { + provide: APP_FILTER, + useClass: GlobalExceptionFilter, + }; +}; diff --git a/server/src/exceptions/exceptions.filter.ts b/server/src/common/exception.filter.ts similarity index 89% rename from server/src/exceptions/exceptions.filter.ts rename to server/src/common/exception.filter.ts index e0dae4c..77bdc40 100644 --- a/server/src/exceptions/exceptions.filter.ts +++ b/server/src/common/exception.filter.ts @@ -10,8 +10,8 @@ import { BaseExceptionFilter } from '@nestjs/core'; import * as util from 'util'; @Catch() -export class ExceptionsFilter extends BaseExceptionFilter { - private readonly logger = new Logger(ExceptionsFilter.name); +export class GlobalExceptionFilter extends BaseExceptionFilter { + private readonly logger = new Logger(GlobalExceptionFilter.name); catch(exception: unknown, host: ArgumentsHost) { if (exception instanceof HttpException) { diff --git a/server/src/common/middleware/session.ts b/server/src/common/middleware/session.ts new file mode 100644 index 0000000..9f62613 --- /dev/null +++ b/server/src/common/middleware/session.ts @@ -0,0 +1,32 @@ +import RedisStore from 'connect-redis'; +import session from 'express-session'; +import Redis from 'ioredis'; + +export const makeRedisStore = () => { + const REDIS_HOST = process.env.REDIS_HOSTNAME; + if (REDIS_HOST == null) throw new Error('REDIS_HOST is not defined'); + + const redis = new Redis({ + host: REDIS_HOST, + port: parseInt(process.env.REDIS_PORT ?? '6379'), + }); + + const redisStore = new RedisStore({ + client: redis, + prefix: 'baekjoonrooms:', + }); + return { redis, redisStore }; +}; + +export const makeSessionMiddleware = (redisStore: RedisStore) => { + const sessionMiddleware = session({ + secret: 'example-session-secret', + resave: false, + saveUninitialized: false, + cookie: { + httpOnly: true, + }, + store: redisStore, + }); + return sessionMiddleware; +}; diff --git a/server/src/common/validation.pipe.ts b/server/src/common/validation.pipe.ts new file mode 100644 index 0000000..b810eb1 --- /dev/null +++ b/server/src/common/validation.pipe.ts @@ -0,0 +1,15 @@ +import { ValidationPipe } from '@nestjs/common'; + +export const globalValidationPipe = () => { + const pipe = new ValidationPipe({ + transform: true, + whitelist: true, + forbidNonWhitelisted: true, + disableErrorMessages: false, // production 환경에서는 보통 true로 설정 + }); + + return { + provide: 'APP_PIPE', + useValue: pipe, + }; +}; diff --git a/server/src/short-logger/short-logger.service.ts b/server/src/logger/custom.logger.ts similarity index 85% rename from server/src/short-logger/short-logger.service.ts rename to server/src/logger/custom.logger.ts index 2937e84..d22529e 100644 --- a/server/src/short-logger/short-logger.service.ts +++ b/server/src/logger/custom.logger.ts @@ -1,7 +1,7 @@ -import { ConsoleLogger, Injectable, LogLevel, Scope } from '@nestjs/common'; +import { ConsoleLogger, LogLevel } from '@nestjs/common'; -@Injectable({ scope: Scope.TRANSIENT }) -export class ShortLoggerService extends ConsoleLogger { +// @Injectable({ scope: Scope.TRANSIENT }) +export class CustomLogger extends ConsoleLogger { protected getTimestamp(): string { const now = new Date(); const month = String(now.getMonth() + 1).padStart(2, '0'); diff --git a/server/src/logger/logger.module.ts b/server/src/logger/logger.module.ts new file mode 100644 index 0000000..1f02ff0 --- /dev/null +++ b/server/src/logger/logger.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { CustomLogger } from './custom.logger'; + +@Module({ + providers: [CustomLogger], + exports: [CustomLogger], +}) +export class LoggerModule {} diff --git a/server/src/main.ts b/server/src/main.ts index 255068a..82cda58 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -1,18 +1,17 @@ -import { ValidationPipe } from '@nestjs/common'; -import { HttpAdapterHost, NestFactory } from '@nestjs/core'; +import { NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; -import RedisStore from 'connect-redis'; -import * as cookieParser from 'cookie-parser'; -import * as session from 'express-session'; -import Redis from 'ioredis'; -import * as morgan from 'morgan'; -import * as passport from 'passport'; +import * as dotenv from 'dotenv'; +import morgan from 'morgan'; import { AppModule } from './app.module'; -import { ExceptionsFilter } from './exceptions/exceptions.filter'; -import { ShortLoggerService } from './short-logger/short-logger.service'; +import { + makeRedisStore, + makeSessionMiddleware, +} from './common/middleware/session'; +import { CustomLogger } from './logger/custom.logger'; import { SocketIOAdapter } from './socket/socket.adapter'; Error.stackTraceLimit = Infinity; +dotenv.config(); async function bootstrap() { const app = await NestFactory.create(AppModule, { @@ -22,53 +21,18 @@ async function bootstrap() { allowedHeaders: 'Content-Type, Accept', credentials: true, }, + bufferLogs: false, }); - app.useLogger(new ShortLoggerService()); + app.useLogger(app.get(CustomLogger)); morganSetup(app); - app.use(cookieParser()); - - const REDIS_HOST = process.env.REDIS_HOSTNAME; - if (REDIS_HOST == null) throw new Error('REDIS_HOST is not defined'); - - const redis = new Redis({ - host: REDIS_HOST, - port: parseInt(process.env.REDIS_PORT ?? '6379'), - }); - - const redisStore = new RedisStore({ - client: redis, - prefix: 'baekjoonrooms:', - }); - - const sessionMiddleware = session({ - secret: 'example-session-secret', - resave: false, - saveUninitialized: false, - cookie: { - httpOnly: true, - }, - store: redisStore, - }); + const { redisStore } = makeRedisStore(); + const sessionMiddleware = makeSessionMiddleware(redisStore); app.use(sessionMiddleware); - - app.use(passport.initialize()); - app.use(passport.session()); - - const { httpAdapter } = app.get(HttpAdapterHost); - app.useGlobalFilters(new ExceptionsFilter(httpAdapter)); - - app.useGlobalPipes( - new ValidationPipe({ - transform: true, - whitelist: true, - forbidNonWhitelisted: true, - disableErrorMessages: false, // production 환경에서는 보통 true로 설정 - }), - ); + app.useWebSocketAdapter(new SocketIOAdapter(sessionMiddleware, app)); const swaggerConfig = new DocumentBuilder() .setTitle('bojrooms API Docs') @@ -78,8 +42,6 @@ async function bootstrap() { const document = SwaggerModule.createDocument(app, swaggerConfig); SwaggerModule.setup('api', app, document); - app.useWebSocketAdapter(new SocketIOAdapter(sessionMiddleware, app)); - await app.listen(4000); } diff --git a/server/src/socket/socket.adapter.ts b/server/src/socket/socket.adapter.ts index cb2ed1a..8ddb9f4 100644 --- a/server/src/socket/socket.adapter.ts +++ b/server/src/socket/socket.adapter.ts @@ -1,7 +1,7 @@ -import { IoAdapter } from '@nestjs/platform-socket.io'; import { INestApplicationContext } from '@nestjs/common'; +import { IoAdapter } from '@nestjs/platform-socket.io'; import { RequestHandler } from 'express'; -import * as passport from 'passport'; +import passport from 'passport'; export class SocketIOAdapter extends IoAdapter { private readonly session: RequestHandler; diff --git a/server/test/app.e2e-spec.ts b/server/test/app.e2e-spec.ts index 868723c..d2cf70c 100644 --- a/server/test/app.e2e-spec.ts +++ b/server/test/app.e2e-spec.ts @@ -1,24 +1,45 @@ import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import * as request from 'supertest'; +import Redis from 'ioredis'; +import { + makeRedisStore, + makeSessionMiddleware, +} from 'src/common/middleware/session'; +import { CustomLogger } from 'src/logger/custom.logger'; +import request from 'supertest'; import { AppModule } from './../src/app.module'; +Error.stackTraceLimit = Infinity; + describe('AppController (e2e)', () => { let app: INestApplication; + let redisClient: Redis; + const logger = new CustomLogger(); - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); + + const { redis, redisStore } = makeRedisStore(); + const sessionMiddleware = makeSessionMiddleware(redisStore); + redisClient = redis; + app.use(sessionMiddleware); + + app.useLogger(logger); + await app.init(); }); - it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect('Hello World!'); + afterAll(async () => { + await app.close(); + await redisClient.quit(); + }); + + it('/ (GET)', async () => { + logger.log('test'); + return request(app.getHttpServer()).get('/').expect(403); }); }); diff --git a/server/tsconfig.json b/server/tsconfig.json index a3a7725..c8a793d 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -1,12 +1,13 @@ { "compilerOptions": { - "module": "commonjs", + "target": "ES2021", + "module": "CommonJS", "declaration": true, "removeComments": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, + "esModuleInterop": true, "allowSyntheticDefaultImports": true, - "target": "ES2021", "sourceMap": true, "outDir": "./dist", "baseUrl": "./",