Skip to content

Commit

Permalink
Merge pull request #60 from boostcampwm-2024/be-feat#59
Browse files Browse the repository at this point in the history
[BE] 기수별 프로젝트 리스트 응답 API 구현
  • Loading branch information
sjy2335 authored Nov 13, 2024
2 parents ef48d07 + 79f600f commit c8da51d
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 138 deletions.
46 changes: 23 additions & 23 deletions backend/console-server/src/config/typeorm.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,27 @@ import { registerAs } from '@nestjs/config';
import type { TypeOrmModuleOptions } from '@nestjs/typeorm';

export default registerAs('typeOrmConfig', () => {
const isDevEnv = ['development', 'test', 'debug'].includes(
process.env.NODE_ENV as string,
);
return (
isDevEnv
? {
type: 'sqlite',
database: ':memory:',
dropSchema: true,
autoLoadEntities: true,
synchronize: true,
}
: {
type: 'mysql',
host: process.env.DB_HOST,
port: process.env.DB_PORT,
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
autoLoadEntities: true,
synchronize: true,
}
) as TypeOrmModuleOptions;
const isDevEnv = ['development', 'test', 'debug'].includes(process.env.NODE_ENV as string);
return (
isDevEnv
? {
type: 'sqlite',
database: ':memory:',
dropSchema: true,
autoLoadEntities: true,
synchronize: true,
logging: ['query', 'error'],
logger: 'advanced-console',
}
: {
type: 'mysql',
host: process.env.DB_HOST,
port: process.env.DB_PORT,
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
autoLoadEntities: true,
synchronize: true,
}
) as TypeOrmModuleOptions;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { IsNotEmpty } from 'class-validator';

export class FindByGenerationResponseDto {
@IsNotEmpty()
name: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { IsNotEmpty, IsNumber } from 'class-validator';
import { Type } from 'class-transformer';

export class FindByGenerationDto {
@IsNotEmpty()
@Type(() => Number)
@IsNumber()
generation: number;
}
25 changes: 14 additions & 11 deletions backend/console-server/src/project/entities/project.entity.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import { Entity, Unique } from 'typeorm';
import { Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity('projects')
@Entity('project')
@Unique(['domain'])
export class Project {
@PrimaryGeneratedColumn()
id: number;
@PrimaryGeneratedColumn()
id: number;

@Column({ type: 'varchar', length: 255 })
name: string;
@Column({ type: 'varchar', length: 255 })
name: string;

@Column({ type: 'varchar', length: 255 })
ip: string;
@Column({ type: 'varchar', length: 255 })
ip: string;

@Column({ type: 'varchar', length: 255, unique: true })
domain: string;
@Column({ type: 'varchar', length: 255, unique: true })
domain: string;

@Column({ type: 'varchar', length: 255 })
email: string;
@Column({ type: 'varchar', length: 255 })
email: string;

@Column({ type: 'int' })
generation: number;
}
21 changes: 14 additions & 7 deletions backend/console-server/src/project/project.controller.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import { Controller, HttpCode, HttpStatus } from '@nestjs/common';
import { Controller, Get, HttpCode, HttpStatus, Param, Query } from '@nestjs/common';
import { Post } from '@nestjs/common';
import { Body } from '@nestjs/common';
import { ProjectService } from './project.service';
import { CreateProjectDto } from './dto/create-project.dto';
import { FindByGenerationDto } from './dto/find-by-generation.dto';

@Controller('project')
export class ProjectController {
constructor(private readonly projectService: ProjectService) {}
constructor(private readonly projectService: ProjectService) {}

@Post()
@HttpCode(HttpStatus.CREATED)
create(@Body() createProjectDto: CreateProjectDto) {
return this.projectService.create(createProjectDto);
}
@Post()
@HttpCode(HttpStatus.CREATED)
create(@Body() createProjectDto: CreateProjectDto) {
return this.projectService.create(createProjectDto);
}

@Get()
@HttpCode(HttpStatus.OK)
findByGeneration(@Query() findGenerationProjectDto: FindByGenerationDto) {
return this.projectService.findByGeneration(findGenerationProjectDto);
}
}
228 changes: 156 additions & 72 deletions backend/console-server/src/project/project.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,81 +9,165 @@ import { QueryFailedError } from 'typeorm';
import type { CreateProjectDto } from './dto/create-project.dto';
import { ProjectResponseDto } from './dto/create-project-response.dto';
import { ConflictException } from '@nestjs/common';
import { FindByGenerationDto } from './dto/find-by-generation.dto';
import { FindByGenerationResponseDto } from './dto/find-by-generation-response.dto';
import { plainToInstance } from 'class-transformer';

describe('ProjectService 클래스의', () => {
let projectService: ProjectService;
let projectRepository: Repository<Project>;
let mailService: MailService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ProjectService,
{
provide: getRepositoryToken(Project),
useValue: {
create: jest.fn(),
save: jest.fn(),
},
},
{
provide: MailService,
useValue: {
sendNameServerInfo: jest.fn(),
},
},
],
}).compile();

projectService = module.get<ProjectService>(ProjectService);
projectRepository = module.get<Repository<Project>>(
getRepositoryToken(Project),
);
mailService = module.get<MailService>(MailService);
});

describe('create() 메소드는', () => {
const createProjectDto: CreateProjectDto = {
name: '테스트 프로젝트',
email: 'test@test.com',
ip: '127.0.0.1',
domain: 'host.test.com',
};

it('올바른 정보가 들어왔을 때 프로젝트를 성공적으로 생성합니다.', async () => {
const projectEntity = { id: 1, ...createProjectDto };
(projectRepository.create as jest.Mock).mockReturnValue(projectEntity);
(projectRepository.save as jest.Mock).mockReturnValue(projectEntity);

const result = await projectService.create(createProjectDto);

expect(projectRepository.create).toHaveBeenCalledWith(createProjectDto);
expect(projectRepository.save).toHaveBeenCalledWith(projectEntity);
expect(mailService.sendNameServerInfo).toHaveBeenCalledWith(
createProjectDto.email,
createProjectDto.name,
);
expect(result).toBeInstanceOf(ProjectResponseDto);
let projectService: ProjectService;
let projectRepository: Repository<Project>;
let mailService: MailService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ProjectService,
{
provide: getRepositoryToken(Project),
useValue: {
create: jest.fn(),
save: jest.fn(),
find: jest.fn(),
},
},
{
provide: MailService,
useValue: {
sendNameServerInfo: jest.fn(),
},
},
],
}).compile();

projectService = module.get<ProjectService>(ProjectService);
projectRepository = module.get<Repository<Project>>(getRepositoryToken(Project));
mailService = module.get<MailService>(MailService);
});
it('이미 존재하는 도메인이 들어오면 ConflictException을 던집니다.', async () => {
const error = new QueryFailedError('query', [], {
code: 'ER_DUP_ENTRY',
} as unknown as Error);
(projectRepository.create as jest.Mock).mockReturnValue({});
(projectRepository.save as jest.Mock).mockRejectedValue(error);

await expect(projectService.create(createProjectDto)).rejects.toThrow(
ConflictException,
);

describe('create() 메소드는', () => {
const createProjectDto: CreateProjectDto = {
name: '테스트 프로젝트',
email: 'test@test.com',
ip: '127.0.0.1',
domain: 'host.test.com',
};

it('올바른 정보가 들어왔을 때 프로젝트를 성공적으로 생성합니다.', async () => {
const projectEntity = { id: 1, ...createProjectDto };

(projectRepository.create as jest.Mock).mockReturnValue(projectEntity);
(projectRepository.save as jest.Mock).mockReturnValue(projectEntity);

const result = await projectService.create(createProjectDto);

expect(projectRepository.create).toHaveBeenCalledWith(createProjectDto);
expect(projectRepository.save).toHaveBeenCalledWith(projectEntity);
expect(mailService.sendNameServerInfo).toHaveBeenCalledWith(
createProjectDto.email,
createProjectDto.name,
);
expect(result).toBeInstanceOf(ProjectResponseDto);
});
it('이미 존재하는 도메인이 들어오면 ConflictException을 던집니다.', async () => {
const error = new QueryFailedError('query', [], {
code: 'ER_DUP_ENTRY',
} as unknown as Error);

(projectRepository.create as jest.Mock).mockReturnValue({});
(projectRepository.save as jest.Mock).mockRejectedValue(error);

await expect(projectService.create(createProjectDto)).rejects.toThrow(
ConflictException,
);
});
it('예상치 못한 오류가 발생하면 해당 오류를 그대로 전달합니다.', async () => {
const error = new Error('예기치 못한 에러');
(projectRepository.create as jest.Mock).mockReturnValue({});
(projectRepository.save as jest.Mock).mockRejectedValue(error);

await expect(projectService.create(createProjectDto)).rejects.toThrow(Error);
});
});
it('예상치 못한 오류가 발생하면 해당 오류를 그대로 전달합니다.', async () => {
const error = new Error('예기치 못한 에러');
(projectRepository.create as jest.Mock).mockReturnValue({});
(projectRepository.save as jest.Mock).mockRejectedValue(error);

await expect(projectService.create(createProjectDto)).rejects.toThrow(
Error,
);

describe('findByGeneration() 메소드는', () => {
it('특정 기수의 프로젝트 이름 목록을 반환합니다.', async () => {
// Given
const generation = 1;
const findGenerationProjectDto: FindByGenerationDto = {
generation,
};

const mockProjects = [
{ name: 'Project A' },
{ name: 'Project B' },
{ name: 'Project C' },
] as Project[];
const expectedResponse = mockProjects.map((p) =>
plainToInstance(FindByGenerationResponseDto, p.name),
);

(projectRepository.find as jest.Mock).mockResolvedValue(mockProjects);

// When
const result = await projectService.findByGeneration(findGenerationProjectDto);

// Then
expect(projectRepository.find).toHaveBeenCalledWith({
select: {
name: true,
},
where: { generation },
});

expect(result).toHaveLength(mockProjects.length);
expect(result).toEqual(expectedResponse);
});

it('프로젝트가 없는 경우 빈 배열을 반환합니다.', async () => {
// Given
const generation = 999;
const findGenerationProjectDto: FindByGenerationDto = {
generation,
};

(projectRepository.find as jest.Mock).mockResolvedValue([]);

// When
const result = await projectService.findByGeneration(findGenerationProjectDto);

// Then
expect(projectRepository.find).toHaveBeenCalledWith({
select: {
name: true,
},
where: { generation },
});

expect(result).toHaveLength(0);
expect(result).toEqual([]);
});

it('리포지토리 에러 발생 시 예외를 던집니다.', async () => {
// Given
const generation = 1;
const findGenerationProjectDto: FindByGenerationDto = {
generation,
};

const mockError = new Error('Database error');
(projectRepository.find as jest.Mock).mockRejectedValue(mockError);

// When & Then
await expect(projectService.findByGeneration(findGenerationProjectDto)).rejects.toThrow(
mockError,
);

expect(projectRepository.find).toHaveBeenCalledWith({
select: {
name: true,
},
where: { generation },
});
});
});
});
});
Loading

0 comments on commit c8da51d

Please sign in to comment.