Skip to content

Commit d18e446

Browse files
authored
[기능] API (#8)
* [기능] auth test 및 기능 추가 (#1) * feat: init auth * feat: add test config for db * feat: add class validator and transformer * feat: add fail test for interviewer * feat: allow global context on mikro orm * feat: make service testable * [기능] login 엔드 포인트 추가 및 e2e 테스트 설정 (#5) * feat: add auth controller test * feat: add e2e test for auth * feat: handle test envrionment for db * feat: make auth api work with edge case test * [기능] reviewer api 추가 (#7) * feat: init reviewers * feat: use inmemory db for test on mikro orm * refac: fix typo and use tests * feat: add reviewers test for controller * feat: add e2e test * fix: rename auth to reviewer * [기능] interview post (#9) * feat: init interviewer * refac: use external folder for fixture * feat: add fail error for interview post * feat: add create api for interview * [기능] interview post controller (#10) * feat: add interview post controller * feat: add e2e test for interview post * [기능] interview get (#11) * feat: make test work with bigint jestjs/jest#11617 * feat: add test for creation error * feat: add test for interviewer id not valid * feat: add test for find all * feat: add test for empty resposne * feat: add test for interview controller * feat: add test for e2e on interview * feat: flush manually * feat: flush manually * feat: add test for interview * [기능] interview 단일 조회 (#12) * feat: add interview * feat: config debug option to false to test 보기 어려워서 지움 * feat: make entity work with detail * refac: use helper function to create interview * [기능] interview contents 추가 (#13) * feat: init interview contents create * feat: add interview contents api * feat: add contents * feat: add interview content get api * feat: add post and find all contents * feat: make conroller work * feat: add test for contents * feat: add e2e test debugger for vscode * feat: add e2e test for create contents * feat: add test for e2e * feat: make end point work
1 parent e0e0dea commit d18e446

36 files changed

+2155
-51
lines changed

.vscode/launch.json

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"name": "Test e2e",
9+
"request": "launch",
10+
"type": "node",
11+
"skipFiles": ["<node_internals>/**"],
12+
"runtimeExecutable": "pnpm",
13+
"runtimeArgs": ["test:e2e"]
14+
}
15+
]
16+
}

package.json

+11-6
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,19 @@
2020
"test:e2e": "jest --config ./test/jest-e2e.json"
2121
},
2222
"dependencies": {
23-
"@mikro-orm/cli": "^6.3.8",
24-
"@mikro-orm/core": "^6.3.8",
25-
"@mikro-orm/entity-generator": "^6.3.8",
26-
"@mikro-orm/migrations": "^6.3.8",
23+
"@mikro-orm/cli": "6.3.8",
24+
"@mikro-orm/core": "6.3.8",
25+
"@mikro-orm/entity-generator": "6.3.8",
26+
"@mikro-orm/migrations": "6.3.8",
2727
"@mikro-orm/nestjs": "^6.0.2",
28-
"@mikro-orm/postgresql": "^6.3.8",
28+
"@mikro-orm/postgresql": "6.3.8",
29+
"@mikro-orm/sqlite": "6.3.8",
2930
"@nestjs/common": "^10.0.0",
3031
"@nestjs/core": "^10.0.0",
32+
"@nestjs/mapped-types": "*",
3133
"@nestjs/platform-express": "^10.0.0",
34+
"class-transformer": "^0.5.1",
35+
"class-validator": "^0.14.1",
3236
"pg": "^8.12.0",
3337
"reflect-metadata": "^0.2.0",
3438
"rxjs": "^7.8.1"
@@ -71,6 +75,7 @@
7175
"**/*.(t|j)s"
7276
],
7377
"coverageDirectory": "../coverage",
74-
"testEnvironment": "node"
78+
"testEnvironment": "node",
79+
"workerThreads": true
7580
}
7681
}

pnpm-lock.yaml

+889-35
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app.module.ts

+16-2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,24 @@ import { Module } from '@nestjs/common';
22
import { AppController } from './app.controller';
33
import { AppService } from './app.service';
44
import { MikroOrmModule } from '@mikro-orm/nestjs';
5-
import mikroOrmConfig from './mikro-orm.config';
5+
import { AuthModule } from './auth/auth.module';
6+
import mikroOrmConfig, { testConfig } from './mikro-orm.config';
7+
import { ReviewersModule } from './reviewers/reviewers.module';
8+
import { InterviewModule } from './interview/interview.module';
9+
10+
BigInt.prototype['toJSON'] = function () {
11+
return this.toString();
12+
};
613

714
@Module({
8-
imports: [MikroOrmModule.forRoot(mikroOrmConfig)],
15+
imports: [
16+
MikroOrmModule.forRoot(
17+
process.env.NODE_ENV === 'test' ? testConfig : mikroOrmConfig,
18+
),
19+
AuthModule,
20+
ReviewersModule,
21+
InterviewModule,
22+
],
923
controllers: [AppController],
1024
providers: [AppService],
1125
})

src/auth/auth.controller.spec.ts

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { AuthController } from './auth.controller';
3+
import { AuthService } from './auth.service';
4+
import { MikroOrmModule } from '@mikro-orm/nestjs';
5+
import { Interviewer } from '../entities/interviewer';
6+
import { testConfig } from '../mikro-orm.config';
7+
import { InterviewerDTO } from './dto/Interviewer.dto';
8+
9+
describe('AuthController', () => {
10+
let controller: AuthController;
11+
12+
beforeEach(async () => {
13+
const module: TestingModule = await Test.createTestingModule({
14+
imports: [
15+
MikroOrmModule.forRoot(testConfig),
16+
MikroOrmModule.forFeature([Interviewer]),
17+
],
18+
controllers: [AuthController],
19+
providers: [AuthService],
20+
}).compile();
21+
22+
controller = module.get<AuthController>(AuthController);
23+
});
24+
25+
it('should be defined', () => {
26+
expect(controller).toBeDefined();
27+
});
28+
29+
it('createInterviewer() should return Interviewer', async () => {
30+
const name = 'testName';
31+
const result = await controller.createInterviewer({ name });
32+
33+
expect(result).toBeInstanceOf(InterviewerDTO);
34+
expect(result.name).toBe(name);
35+
});
36+
});

src/auth/auth.controller.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Body, Controller, Post } from '@nestjs/common';
2+
import { AuthService } from './auth.service';
3+
import { CreateInterviewerDTO } from './dto/createInterviewer.dto';
4+
5+
@Controller('login')
6+
export class AuthController {
7+
constructor(private readonly authService: AuthService) {}
8+
9+
@Post()
10+
createInterviewer(@Body() dto: CreateInterviewerDTO) {
11+
return this.authService.createInterviewer(dto);
12+
}
13+
}

src/auth/auth.module.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Module } from '@nestjs/common';
2+
import { AuthService } from './auth.service';
3+
import { AuthController } from './auth.controller';
4+
import { MikroOrmModule } from '@mikro-orm/nestjs';
5+
import { Interviewer } from '../entities/interviewer';
6+
7+
@Module({
8+
imports: [MikroOrmModule.forFeature([Interviewer])],
9+
controllers: [AuthController],
10+
providers: [AuthService],
11+
})
12+
export class AuthModule {}

src/auth/auth.service.spec.ts

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { AuthService } from './auth.service';
3+
import { getRepositoryToken, MikroOrmModule } from '@mikro-orm/nestjs';
4+
import { Interviewer } from '../entities/interviewer';
5+
import { CreateInterviewerDTO } from './dto/createInterviewer.dto';
6+
import { plainToInstance } from 'class-transformer';
7+
import { testConfig } from '../mikro-orm.config';
8+
import { InterviewerDTO } from './dto/Interviewer.dto';
9+
10+
describe('AuthService', () => {
11+
let service: AuthService;
12+
13+
beforeEach(async () => {
14+
const module: TestingModule = await Test.createTestingModule({
15+
imports: [
16+
MikroOrmModule.forRoot(testConfig),
17+
MikroOrmModule.forFeature([Interviewer]),
18+
],
19+
providers: [AuthService],
20+
}).compile();
21+
22+
service = module.get<AuthService>(AuthService);
23+
});
24+
25+
it('should be defined', () => {
26+
expect(service).toBeDefined();
27+
});
28+
29+
it('20자 미만, 0자 이상의 입력에 대해서 interviewer를 생성함', async () => {
30+
const name = 'validName';
31+
const createDTO = plainToInstance(CreateInterviewerDTO, { name });
32+
33+
const interviewer = await service.createInterviewer(createDTO);
34+
35+
expect(interviewer).toBeInstanceOf(InterviewerDTO);
36+
expect(interviewer.name).toEqual(name);
37+
});
38+
});

src/auth/auth.service.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { CreateInterviewerDTO } from './dto/createInterviewer.dto';
3+
import { Interviewer } from '../entities/interviewer';
4+
import { EntityRepository } from '@mikro-orm/core';
5+
import { InjectRepository } from '@mikro-orm/nestjs';
6+
import { InterviewerDTO } from './dto/Interviewer.dto';
7+
8+
@Injectable()
9+
export class AuthService {
10+
constructor(
11+
@InjectRepository(Interviewer)
12+
private readonly interviewerRepository: EntityRepository<Interviewer>,
13+
) {}
14+
15+
async createInterviewer(createDTO: CreateInterviewerDTO) {
16+
const interviewer = await this.interviewerRepository.create(createDTO);
17+
18+
await this.interviewerRepository
19+
.getEntityManager()
20+
.persistAndFlush(interviewer);
21+
22+
return InterviewerDTO.fromEntity(interviewer);
23+
}
24+
}

src/auth/dto/Interviewer.dto.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Exclude, Expose } from 'class-transformer';
2+
import { Interviewer } from 'src/entities/interviewer';
3+
4+
@Exclude()
5+
export class InterviewerDTO {
6+
@Expose()
7+
name: string;
8+
9+
static fromEntity(entity: Interviewer): InterviewerDTO {
10+
const interviewerDTO = new InterviewerDTO();
11+
interviewerDTO.name = entity.name;
12+
13+
return interviewerDTO;
14+
}
15+
}

src/auth/dto/createInterviewer.dto.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Exclude, Expose } from 'class-transformer';
2+
import { Length, IsString } from 'class-validator';
3+
4+
@Exclude()
5+
export class CreateInterviewerDTO {
6+
@Expose()
7+
@IsString()
8+
@Length(1, 20)
9+
name: string;
10+
}

src/entities/interview.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1-
import { Entity, Property, ManyToOne, PrimaryKey } from '@mikro-orm/core';
1+
import {
2+
Entity,
3+
Property,
4+
ManyToOne,
5+
PrimaryKey,
6+
OneToMany,
7+
Collection,
8+
} from '@mikro-orm/core';
29
import { Reviewer } from './reviewer';
310
import { Interviewer } from './interviewer';
11+
import { InterviewContents } from './interviewContents';
412

513
@Entity()
614
export class Interview {
@@ -18,4 +26,11 @@ export class Interview {
1826

1927
@Property({ length: 10 })
2028
status: string;
29+
30+
@OneToMany({
31+
entity: () => InterviewContents,
32+
mappedBy: 'interview',
33+
orphanRemoval: true,
34+
})
35+
contents = new Collection<InterviewContents>(this);
2136
}

src/entities/interviewContents.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Entity, Property, ManyToOne, PrimaryKey } from '@mikro-orm/core';
2-
import { Interviewer } from './interviewer';
2+
import { Interview } from './interview';
33

44
@Entity()
55
export class InterviewContents {
@@ -12,6 +12,6 @@ export class InterviewContents {
1212
@Property({ length: 20 })
1313
speaker: string;
1414

15-
@ManyToOne()
16-
interview: Interviewer;
15+
@ManyToOne({ entity: () => Interview })
16+
interview: Interview;
1717
}

src/entities/interviewer.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Entity, Property, ManyToOne, PrimaryKey } from '@mikro-orm/core';
1+
import { Entity, Property, PrimaryKey } from '@mikro-orm/core';
22

33
@Entity()
44
export class Interviewer {
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export class CreateInterviewDTO {
2+
interviewerId: bigint;
3+
reviewerId: bigint;
4+
5+
static from({
6+
interviewerId,
7+
reviewerId,
8+
}: {
9+
interviewerId: bigint;
10+
reviewerId: bigint;
11+
}) {
12+
const interviewDTO = new CreateInterviewDTO();
13+
14+
interviewDTO.interviewerId = interviewerId;
15+
interviewDTO.reviewerId = reviewerId;
16+
17+
return interviewDTO;
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export class CreateInterviewContentDTO {
2+
content: string;
3+
}
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { InterviewContents } from '../../entities/interviewContents';
2+
3+
export class InterviewContentDTO {
4+
id: bigint;
5+
speaker: string;
6+
content: string;
7+
8+
static fromEntity(entity: InterviewContents) {
9+
const interviewContentDTO = new InterviewContentDTO();
10+
interviewContentDTO.id = entity.id;
11+
interviewContentDTO.speaker = entity.speaker;
12+
interviewContentDTO.content = entity.content;
13+
14+
return interviewContentDTO;
15+
}
16+
}
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Interview } from '../../entities/interview';
2+
import { InterviewContentDTO } from './interviewContent.dto';
3+
4+
export class InterviewDetailDTO {
5+
id: bigint;
6+
title: string;
7+
status: string;
8+
contents: InterviewContentDTO[];
9+
10+
static fromEntity(entity: Interview) {
11+
const interviewDetailDTO = new InterviewDetailDTO();
12+
interviewDetailDTO.id = entity.id;
13+
interviewDetailDTO.title = entity.title;
14+
interviewDetailDTO.status = entity.status;
15+
interviewDetailDTO.contents = entity.contents.map(
16+
InterviewContentDTO.fromEntity,
17+
);
18+
19+
return interviewDetailDTO;
20+
}
21+
}

0 commit comments

Comments
 (0)