Skip to content

Commit

Permalink
server E2E test 기반 테스트 작성 및 동작 확인 성공 (#279)
Browse files Browse the repository at this point in the history
* 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!
  • Loading branch information
vimkim authored Feb 11, 2024
1 parent 5be6298 commit 8240826
Show file tree
Hide file tree
Showing 14 changed files with 163 additions and 89 deletions.
18 changes: 15 additions & 3 deletions server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
35 changes: 26 additions & 9 deletions server/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -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,
}),
Expand All @@ -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('*');
}
}
14 changes: 5 additions & 9 deletions server/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
9 changes: 9 additions & 0 deletions server/src/common/exception.filter.provider.ts
Original file line number Diff line number Diff line change
@@ -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,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
32 changes: 32 additions & 0 deletions server/src/common/middleware/session.ts
Original file line number Diff line number Diff line change
@@ -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;
};
15 changes: 15 additions & 0 deletions server/src/common/validation.pipe.ts
Original file line number Diff line number Diff line change
@@ -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,
};
};
Original file line number Diff line number Diff line change
@@ -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');
Expand Down
8 changes: 8 additions & 0 deletions server/src/logger/logger.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { CustomLogger } from './custom.logger';

@Module({
providers: [CustomLogger],
exports: [CustomLogger],
})
export class LoggerModule {}
66 changes: 14 additions & 52 deletions server/src/main.ts
Original file line number Diff line number Diff line change
@@ -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, {
Expand All @@ -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')
Expand All @@ -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);
}

Expand Down
4 changes: 2 additions & 2 deletions server/src/socket/socket.adapter.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
35 changes: 28 additions & 7 deletions server/test/app.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading

0 comments on commit 8240826

Please sign in to comment.