Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[server] ci/cd test ver3 #53

Merged
merged 7 commits into from
Nov 20, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# 디폴트 무시된 파일
/.idea/shelf/
/.idea/workspace.xml
# 에디터 기반 HTTP 클라이언트 요청
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

/.idea
4 changes: 4 additions & 0 deletions server/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.gitignore
Dockerfile
node_modules
dist
25 changes: 25 additions & 0 deletions server/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};
38 changes: 38 additions & 0 deletions server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# compiled output
/dist
/node_modules

# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# OS
.DS_Store

# Tests
/coverage
/.nyc_output

# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace

# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

.env
postgres-data
4 changes: 4 additions & 0 deletions server/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}
10 changes: 10 additions & 0 deletions server/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM node:18
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

COPY . .
RUN yarn install
RUN yarn run build

EXPOSE 3000
CMD ["yarn", "start:dev"]
73 changes: 73 additions & 0 deletions server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
</p>

[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest

<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->

## Description

[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.

## Installation

```bash
$ yarn install
```

## Running the app

```bash
# development
$ yarn run start

# watch mode
$ yarn run start:dev

# production mode
$ yarn run start:prod
```

## Test

```bash
# unit tests
$ yarn run test

# e2e tests
$ yarn run test:e2e

# test coverage
$ yarn run test:cov
```

## Support

Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).

## Stay in touch

- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)

## License

Nest is [MIT licensed](LICENSE).
30 changes: 30 additions & 0 deletions server/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
version: '3.8'
services:
postgresql_db:
image: postgres:15
restart: always
volumes:
- ./postgres-data:/var/lib/postgresql/data
ports:
- '5432:5432'
environment:
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_DATABASE}

# nestjs_server:
# build: .
# ports:
# - '3000:3000'
# depends_on:
# - postgresql_db
# environment:
# JWT_SECRET: ${JWT_SECRET}
# HASH_ROUNDS: ${HASH_ROUNDS}
# PROTOCOL: ${PROTOCOL}
# HOST: ${HOST}
# DB_HOST: ${DB_HOST}
# DB_PORT: ${DB_PORT}
# DB_USERNAME: ${DB_USERNAME}
# DB_PASSWORD: ${DB_PASSWORD}
# DB_DATABASE: ${DB_DATABASE}
8 changes: 8 additions & 0 deletions server/nest-cli.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}
80 changes: 80 additions & 0 deletions server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{
"name": "server",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.0.0",
"@nestjs/mapped-types": "*",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/typeorm": "^10.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"pg": "^8.11.3",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"typeorm": "^0.3.17"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"moduleNameMapper": {
"^src/(.*)$": "<rootDir>/$1",
"^test/(.*)$": "<rootDir>/../test/$1"
},
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
22 changes: 22 additions & 0 deletions server/src/app.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';

describe('AppController', () => {
let appController: AppController;

beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();

appController = app.get<AppController>(AppController);
});

describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});
12 changes: 12 additions & 0 deletions server/src/app.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}

@Get()
getHello(): string {
return this.appService.getHello();
}
}
35 changes: 35 additions & 0 deletions server/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { CommonModule } from './common/common.module';
import { UsersModule } from './users/users.module';
import { UserModel } from './users/entities/user.entity';
import { FoldersModule } from './folders/folders.module';
import { FolderModel } from './folders/entities/folder.entity';

@Module({
imports: [
ConfigModule.forRoot({
envFilePath: '.env',
isGlobal: true,
}),
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env['DB_HOST'],
port: parseInt(process.env['DB_PORT']),
username: process.env['DB_USERNAME'],
password: process.env['DB_PASSWORD'],
database: process.env['DB_DATABASE'],
entities: [UserModel, FolderModel],
synchronize: true, // DO NOT USE IN PRODUCTION
}),
CommonModule,
UsersModule,
FoldersModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
8 changes: 8 additions & 0 deletions server/src/app.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
20 changes: 20 additions & 0 deletions server/src/common/common.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CommonController } from './common.controller';
import { CommonService } from './common.service';

describe('CommonController', () => {
let controller: CommonController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [CommonController],
providers: [CommonService],
}).compile();

controller = module.get<CommonController>(CommonController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
7 changes: 7 additions & 0 deletions server/src/common/common.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Controller } from '@nestjs/common';
import { CommonService } from './common.service';

@Controller('common')
export class CommonController {
constructor(private readonly commonService: CommonService) {}
}
9 changes: 9 additions & 0 deletions server/src/common/common.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { CommonService } from './common.service';
import { CommonController } from './common.controller';

@Module({
controllers: [CommonController],
providers: [CommonService],
})
export class CommonModule {}
18 changes: 18 additions & 0 deletions server/src/common/common.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CommonService } from './common.service';

describe('CommonService', () => {
let service: CommonService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CommonService],
}).compile();

service = module.get<CommonService>(CommonService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
4 changes: 4 additions & 0 deletions server/src/common/common.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';

@Injectable()
export class CommonService {}
16 changes: 16 additions & 0 deletions server/src/common/entity/base.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {
CreateDateColumn,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';

export abstract class BaseModel {
@PrimaryGeneratedColumn()
id: number;

@UpdateDateColumn()
updatedAt: Date;

@CreateDateColumn()
createdAt: Date;
}
7 changes: 7 additions & 0 deletions server/src/folders/dto/create-folder.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { IsString } from 'class-validator';

export class CreateFolderDto {
// 길이 제한 필요
@IsString()
title: string;
}
4 changes: 4 additions & 0 deletions server/src/folders/dto/update-folder.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { PickType } from '@nestjs/mapped-types';
import { CreateFolderDto } from './create-folder.dto';

export class UpdateFolderDto extends PickType(CreateFolderDto, ['title']) {}
8 changes: 8 additions & 0 deletions server/src/folders/entities/folder.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { BaseModel } from '../../common/entity/base.entity';
import { Column, Entity } from 'typeorm';

@Entity()
export class FolderModel extends BaseModel {
@Column({ default: '새 폴더' })
title: string;
}
42 changes: 42 additions & 0 deletions server/src/folders/folders.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
Controller,
Get,
Post,
Body,
Param,
Delete,
Put,
} from '@nestjs/common';
import { FoldersService } from './folders.service';
import { CreateFolderDto } from './dto/create-folder.dto';
import { UpdateFolderDto } from './dto/update-folder.dto';

@Controller('folders')
export class FoldersController {
constructor(private readonly foldersService: FoldersService) {}

@Post()
postFolder(@Body() createFolderDto: CreateFolderDto) {
return this.foldersService.createFolder(createFolderDto);
}

@Get()
getFolders() {
return this.foldersService.findAllFolders();
}

@Get(':id')
getFolder(@Param('id') id: number) {
return this.foldersService.findFolderById(id);
}

@Put(':id')
update(@Param('id') id: number, @Body() updateFolderDto: UpdateFolderDto) {
return this.foldersService.updateFolder(id, updateFolderDto);
}

@Delete(':id')
remove(@Param('id') id: number) {
return this.foldersService.removeFolder(id);
}
}
12 changes: 12 additions & 0 deletions server/src/folders/folders.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { FoldersService } from './folders.service';
import { FoldersController } from './folders.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FolderModel } from './entities/folder.entity';

@Module({
imports: [TypeOrmModule.forFeature([FolderModel])],
controllers: [FoldersController],
providers: [FoldersService],
})
export class FoldersModule {}
154 changes: 154 additions & 0 deletions server/src/folders/folders.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { Test, TestingModule } from '@nestjs/testing';
import { FoldersService } from './folders.service';
import { FolderModel } from './entities/folder.entity';
import { Repository } from 'typeorm';
import { BadRequestException } from '@nestjs/common';
import { getRepositoryToken } from '@nestjs/typeorm';
import { CreateFolderDto } from './dto/create-folder.dto';
import { UpdateFolderDto } from './dto/update-folder.dto';

type MockRepository<T = any> = Partial<Record<keyof Repository<T>, jest.Mock>>;

describe('FoldersService', () => {
let service: FoldersService;
let mockFoldersRepository: MockRepository<FolderModel>;

beforeEach(async () => {
mockFoldersRepository = {
create: jest.fn(),
save: jest.fn(),
findOne: jest.fn(),
find: jest.fn(),
remove: jest.fn(),
exist: jest.fn(),
};

const module: TestingModule = await Test.createTestingModule({
providers: [
FoldersService,
{
provide: getRepositoryToken(FolderModel),
useValue: mockFoldersRepository,
},
],
}).compile();

service = module.get<FoldersService>(FoldersService);
});

it('service.createFolder(createFolderDto) : 새로운 폴더를 생성한다.', async () => {
const createFolderDto: CreateFolderDto = {
title: 'blackpink in your area',
};
mockFoldersRepository.exist.mockResolvedValue(false);
mockFoldersRepository.create.mockReturnValue(createFolderDto);
mockFoldersRepository.save.mockResolvedValue({ id: 1, ...createFolderDto });

const result = await service.createFolder(createFolderDto);

expect(mockFoldersRepository.exist).toHaveBeenCalledWith({
where: { title: createFolderDto.title },
});
expect(mockFoldersRepository.create).toHaveBeenCalledWith(createFolderDto);
expect(mockFoldersRepository.save).toHaveBeenCalledWith(createFolderDto);
expect(result).toEqual({ id: 1, ...createFolderDto });
});

it('service.createFolder(createFolderDto) : 이미 존재하는 폴더명일 경우 BadRequestException을 던진다.', async () => {
const createFolderDto: CreateFolderDto = {
title: 'blackpink in your area',
};
mockFoldersRepository.exist.mockResolvedValue(true);

await expect(service.createFolder(createFolderDto)).rejects.toThrow(
BadRequestException,
);
});

it('service.findAllFolders() : 모든 폴더를 찾는다.', async () => {
const mockFolders = [
{ id: 1, email: 'test@example.com', nickname: 'TestFolder' },
{ id: 2, email: 'test2@example.com', nickname: 'TestFolder2' },
];
mockFoldersRepository.find.mockResolvedValue(mockFolders);

const result = await service.findAllFolders();

expect(mockFoldersRepository.find).toHaveBeenCalled();
expect(result).toEqual(mockFolders);
});

it('service.findFolderById(id) : id에 해당하는 폴더를 찾는다.', async () => {
const folder = { id: 1, title: 'blackpink in your area' };
mockFoldersRepository.findOne.mockResolvedValue(folder);

const result = await service.findFolderById(1);

expect(mockFoldersRepository.findOne).toHaveBeenCalledWith({
where: { id: 1 },
});
expect(result).toEqual(folder);
});

it('service.findFolderById(id) : 존재하지 않는 폴더명일 경우 BadRequestException을 던진다.', async () => {
mockFoldersRepository.findOne.mockResolvedValue(null);

await expect(service.findFolderById(1)).rejects.toThrow(
BadRequestException,
);
});

it('service.updateFolder(id, updateFolderDto) : id에 해당하는 폴더를 업데이트한다.', async () => {
const updateFolderDto: UpdateFolderDto = {
title: 'newJeans in your area',
};
const existingFolder = {
id: 1,
title: 'blackpink in your area',
};
mockFoldersRepository.findOne.mockResolvedValue(existingFolder);
mockFoldersRepository.save.mockResolvedValue({
...existingFolder,
...updateFolderDto,
});

const result = await service.updateFolder(1, updateFolderDto);

expect(mockFoldersRepository.findOne).toHaveBeenCalledWith({
where: { id: 1 },
});
expect(mockFoldersRepository.save).toHaveBeenCalledWith({
...existingFolder,
...updateFolderDto,
});
expect(result.title).toEqual('newJeans in your area');
});

it('service.updateFolder(id, updateFolderDto) : 존재하지 않는 폴더명일 경우 BadRequestException을 던진다.', async () => {
const updateFolderDto: UpdateFolderDto = { title: 'UpdatedFolder' };
mockFoldersRepository.findOne.mockResolvedValue(null);

await expect(service.updateFolder(1, updateFolderDto)).rejects.toThrow(
BadRequestException,
);
});

it('service.removeFolder(id) : id에 해당하는 폴더를 삭제한다.', async () => {
const folder = { id: 1, title: 'blackpink in your area' };
mockFoldersRepository.findOne.mockResolvedValue(folder);
mockFoldersRepository.remove.mockResolvedValue(folder);

const result = await service.removeFolder(1);

expect(mockFoldersRepository.findOne).toHaveBeenCalledWith({
where: { id: 1 },
});
expect(mockFoldersRepository.remove).toHaveBeenCalledWith(folder);
expect(result).toEqual({ message: '삭제되었습니다.' });
});

it('service.removeFolder(id) : 존재하지 않는 폴더명일 경우 BadRequestException을 던진다.', async () => {
mockFoldersRepository.findOne.mockResolvedValue(null);
await expect(service.removeFolder(1)).rejects.toThrow(BadRequestException);
});
});
55 changes: 55 additions & 0 deletions server/src/folders/folders.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { CreateFolderDto } from './dto/create-folder.dto';
import { UpdateFolderDto } from './dto/update-folder.dto';
import { FolderModel } from './entities/folder.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

@Injectable()
export class FoldersService {
constructor(
@InjectRepository(FolderModel)
private folderRepository: Repository<FolderModel>,
) {}
async createFolder(createFolderDto: CreateFolderDto) {
const folderObject = this.folderRepository.create(createFolderDto);
const folderExists = await this.folderRepository.exist({
where: {
title: createFolderDto.title,
},
});
if (folderExists) {
throw new BadRequestException('이미 존재하는 폴더입니다.');
}
const newFolder = await this.folderRepository.save(folderObject);
return newFolder;
}

async findAllFolders() {
const folders = await this.folderRepository.find();
return folders;
}

async findFolderById(id: number) {
const folder = await this.folderRepository.findOne({ where: { id } });
if (!folder) {
throw new BadRequestException('존재하지 않는 폴더입니다.');
}
return folder;
}

async updateFolder(id: number, updateFolderDto: UpdateFolderDto) {
const folder = await this.findFolderById(id);
const updatedFolder = await this.folderRepository.save({
...folder,
...updateFolderDto,
});
return updatedFolder;
}

async removeFolder(id: number) {
const folder = await this.findFolderById(id);
await this.folderRepository.remove(folder);
return { message: '삭제되었습니다.' };
}
}
19 changes: 19 additions & 0 deletions server/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
transform: true, // 요청에서 넘어온 자료들의 형변환을 자동으로 해줌
transformOptions: {
enableImplicitConversion: true, // true로 설정하면, 자동 형변환을 허용함
},
whitelist: true, // 데코레이터가 없는 속성들은 제거해줌
forbidNonWhitelisted: true, // 데코레이터가 없는 속성이 있으면 요청 자체를 막아버림
}),
);
await app.listen(3000);
}
bootstrap();
9 changes: 9 additions & 0 deletions server/src/users/dto/create-user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { IsEmail, IsString } from 'class-validator';

export class CreateUserDto {
@IsEmail()
email: string;

@IsString()
nickname: string;
}
4 changes: 4 additions & 0 deletions server/src/users/dto/update-user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { PickType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';

export class UpdateUserDto extends PickType(CreateUserDto, ['nickname']) {}
21 changes: 21 additions & 0 deletions server/src/users/entities/user.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { BaseModel } from 'src/common/entity/base.entity';
import { Column, Entity, Generated } from 'typeorm';

@Entity()
export class UserModel extends BaseModel {
@Column({ unique: true })
email: string;

@Column()
@Generated('uuid') // 임시로 uuid를 생성해줌(원래는 provider의 고유 id를 받아와야함)
providerId: string;

@Column({ default: 'APPLE' })
provider: string;

@Column()
nickname: string;

@Column({ default: 'testimagelink' })
profileImage: string;
}
24 changes: 24 additions & 0 deletions server/src/users/users.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { TestCommonModule } from 'test/test-common.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserModel } from './entities/user.entity';

describe('UsersController', () => {
let controller: UsersController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [TestCommonModule, TypeOrmModule.forFeature([UserModel])],
controllers: [UsersController],
providers: [UsersService],
}).compile();

controller = module.get<UsersController>(UsersController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
43 changes: 43 additions & 0 deletions server/src/users/users.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
Put,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}

@Post()
postUser(@Body() createUserDto: CreateUserDto) {
return this.usersService.createUser(createUserDto);
}

@Get()
getUsers() {
return this.usersService.findAllUsers();
}

@Get(':id')
getUser(@Param('id') id: string) {
return this.usersService.findUserById(+id);
}

@Put(':id')
putUser(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.usersService.updateUser(+id, updateUserDto);
}

@Delete(':id')
deleteUser(@Param('id') id: string) {
return this.usersService.removeUser(+id);
}
}
12 changes: 12 additions & 0 deletions server/src/users/users.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserModel } from './entities/user.entity';

@Module({
imports: [TypeOrmModule.forFeature([UserModel])],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
153 changes: 153 additions & 0 deletions server/src/users/users.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { UserModel } from './entities/user.entity';
import { CreateUserDto } from './dto/create-user.dto';
import { BadRequestException } from '@nestjs/common';
import { UpdateUserDto } from './dto/update-user.dto';
import { Repository } from 'typeorm';

type MockRepository<T = any> = Partial<Record<keyof Repository<T>, jest.Mock>>;

describe('UsersService', () => {
let service: UsersService;
let mockUsersRepository: MockRepository<UserModel>;

beforeEach(async () => {
mockUsersRepository = {
create: jest.fn(),
save: jest.fn(),
findOne: jest.fn(),
find: jest.fn(),
remove: jest.fn(),
exist: jest.fn(),
};

const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: getRepositoryToken(UserModel),
useValue: mockUsersRepository,
},
],
}).compile();

service = module.get<UsersService>(UsersService);
});

it('service.createUser(createUserDto) : 새로운 유저를 생성한다.', async () => {
const createUserDto: CreateUserDto = {
email: 'test@example.com',
nickname: 'TestUser',
};
mockUsersRepository.exist.mockResolvedValue(false);
mockUsersRepository.create.mockReturnValue(createUserDto);
mockUsersRepository.save.mockResolvedValue({ id: 1, ...createUserDto });

const result = await service.createUser(createUserDto);

expect(mockUsersRepository.exist).toHaveBeenCalledWith({
where: { email: createUserDto.email },
});
expect(mockUsersRepository.create).toHaveBeenCalledWith(createUserDto);
expect(mockUsersRepository.save).toHaveBeenCalledWith(createUserDto);
expect(result).toEqual({ id: 1, ...createUserDto });
});

it('service.createUser(createUserDto) : 이미 존재하는 이메일일 경우 BadRequestException을 던진다.', async () => {
const createUserDto: CreateUserDto = {
email: 'test@example.com',
nickname: 'TestUser',
};
mockUsersRepository.exist.mockResolvedValue(true);

await expect(service.createUser(createUserDto)).rejects.toThrow(
BadRequestException,
);
});

it('service.findAllUsers() : 모든 유저를 찾는다.', async () => {
const mockUsers = [
{ id: 1, email: 'test@example.com', nickname: 'TestUser' },
{ id: 2, email: 'test2@example.com', nickname: 'TestUser2' },
];
mockUsersRepository.find.mockResolvedValue(mockUsers);

const result = await service.findAllUsers();

expect(mockUsersRepository.find).toHaveBeenCalled();
expect(result).toEqual(mockUsers);
});

it('service.findUserById(id) : id에 해당하는 유저를 찾는다.', async () => {
const user = { id: 1, email: 'test@example.com', nickname: 'TestUser' };
mockUsersRepository.findOne.mockResolvedValue(user);

const result = await service.findUserById(1);

expect(mockUsersRepository.findOne).toHaveBeenCalledWith({
where: { id: 1 },
});
expect(result).toEqual(user);
});

it('service.findUserById(id) : 존재하지 않는 유저일 경우 BadRequestException을 던진다.', async () => {
mockUsersRepository.findOne.mockResolvedValue(null);

await expect(service.findUserById(1)).rejects.toThrow(BadRequestException);
});

it('service.updateUser(id, updateUserDto) : id에 해당하는 유저를 업데이트한다.', async () => {
const updateUserDto: UpdateUserDto = { nickname: 'UpdatedUser' };
const existingUser = {
id: 1,
email: 'test@example.com',
nickname: 'TestUser',
};
mockUsersRepository.findOne.mockResolvedValue(existingUser);
mockUsersRepository.save.mockResolvedValue({
...existingUser,
...updateUserDto,
});

const result = await service.updateUser(1, updateUserDto);

expect(mockUsersRepository.findOne).toHaveBeenCalledWith({
where: { id: 1 },
});
expect(mockUsersRepository.save).toHaveBeenCalledWith({
...existingUser,
...updateUserDto,
});
expect(result.nickname).toEqual('UpdatedUser');
});

it('service.updateUser(id, updateUserDto) : 존재하지 않는 유저일 경우 BadRequestException을 던진다.', async () => {
const updateUserDto: UpdateUserDto = { nickname: 'UpdatedUser' };
mockUsersRepository.findOne.mockResolvedValue(null);

await expect(service.updateUser(1, updateUserDto)).rejects.toThrow(
BadRequestException,
);
});

it('service.removeUser(id) : id에 해당하는 유저를 삭제한다.', async () => {
const user = { id: 1, email: 'test@example.com', nickname: 'TestUser' };
mockUsersRepository.findOne.mockResolvedValue(user);
mockUsersRepository.remove.mockResolvedValue(user);

const result = await service.removeUser(1);

expect(mockUsersRepository.findOne).toHaveBeenCalledWith({
where: { id: 1 },
});
expect(mockUsersRepository.remove).toHaveBeenCalledWith(user);
expect(result).toEqual({ message: '삭제되었습니다.' });
});

it('service.removeUser(id) : 존재하지 않는 유저일 경우 BadRequestException을 던진다.', async () => {
mockUsersRepository.findOne.mockResolvedValue(null);
await expect(service.removeUser(1)).rejects.toThrow(BadRequestException);
});
});
55 changes: 55 additions & 0 deletions server/src/users/users.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { UserModel } from './entities/user.entity';
import { Repository } from 'typeorm';

@Injectable()
export class UsersService {
constructor(
@InjectRepository(UserModel)
private readonly usersRepository: Repository<UserModel>,
) {}
async createUser(createUserDto: CreateUserDto) {
const userObject = this.usersRepository.create(createUserDto);
const emailExists = await this.usersRepository.exist({
where: {
email: createUserDto.email,
},
});
if (emailExists) {
throw new BadRequestException('이미 존재하는 이메일입니다.');
}
const newUser = await this.usersRepository.save(userObject);
return newUser;
}

async findAllUsers() {
const users = await this.usersRepository.find();
return users;
}

async findUserById(id: number) {
const user = await this.usersRepository.findOne({ where: { id } });
if (!user) {
throw new BadRequestException('존재하지 않는 유저입니다.');
}
return user;
}

async updateUser(id: number, updateUserDto: UpdateUserDto) {
const user = await this.findUserById(id);
const updatedUser = await this.usersRepository.save({
...user,
...updateUserDto,
});
return updatedUser;
}

async removeUser(id: number) {
const user = await this.findUserById(id);
await this.usersRepository.remove(user);
return { message: '삭제되었습니다.' };
}
}
24 changes: 24 additions & 0 deletions server/test/app.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';

describe('AppController (e2e)', () => {
let app: INestApplication;

beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();

app = moduleFixture.createNestApplication();
await app.init();
});

it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});
9 changes: 9 additions & 0 deletions server/test/jest-e2e.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}
24 changes: 24 additions & 0 deletions server/test/test-common.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { UserModel } from 'src/users/entities/user.entity';

@Module({
imports: [
ConfigModule.forRoot({
envFilePath: '.env',
isGlobal: true,
}),
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env['DB_HOST'],
port: parseInt(process.env['DB_PORT']),
username: process.env['DB_USERNAME'],
password: process.env['DB_PASSWORD'],
database: process.env['DB_DATABASE'],
entities: [UserModel],
synchronize: true,
}),
],
})
export class TestCommonModule {}
4 changes: 4 additions & 0 deletions server/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}
21 changes: 21 additions & 0 deletions server/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
}
}
5,436 changes: 5,436 additions & 0 deletions server/yarn.lock

Large diffs are not rendered by default.