Skip to content

Commit

Permalink
Feature/local register (#7) user register api & unit test
Browse files Browse the repository at this point in the history
* feat: 全域驗證器默認設定

* feat: 使用者資料表初始化

* feat: 使用者註冊驗證

* feat: 使用者註冊資料儲存

* feat: 使用者註冊密碼加密

* feat: 使用者註冊 swagger demo

* feat: 使用者註冊加上回應狀態

* test: auth service unit test

* test: auth controller 單元測試-dto資料驗證

* test: auth controller 單元測試-創建本地使用者201回應

* test: auth controller 單元測試-創建本地使用者資料驗證失敗406回應

* test: auth controller 單元測試-providers 資料庫命名錯誤修正

* test: auth controller 單元測試-創建本地使用者資料重覆失敗409回應

* test: auth service 單元測試加上測試後復原資料庫

* test: 將測試資料庫改成 sqlite

* test: 將 afterEach 搬移至最後面並只需要一次

* fix: 將非此次測試的刪除

* fix: 修正狀態碼 406->400

* refactor: 將 createAt 改成 createdAt

* fix: 範例訊息移除 name 的重複衝突

* test: usersService 單元測試-創建使用者

* refactor: 將 try catch 移除

* refactor: 更改為使用 hashSync

* refactor: 使用單數 user

* fix: auth controller 單元測試-重新撰寫是否會正確驗證傳入的 payload

* refactor: 將 DTO 驗證搬移出去

* refactor: 將自定義的 validationPipe 獨立出來

* refactor: 應使用 pips 命名資料夾

* refactor: 刪除多餘命名

* refactor: 驗證 dto 使用自訂 validation-pipe
  • Loading branch information
a20688392 authored Jun 10, 2023
1 parent 55e5ba9 commit 2f13ea2
Show file tree
Hide file tree
Showing 21 changed files with 1,245 additions and 18 deletions.
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test": "jest --verbose",
"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",
Expand All @@ -39,6 +39,7 @@
"@nestjs/platform-express": "^9.0.0",
"@nestjs/swagger": "^6.1.4",
"@nestjs/typeorm": "^9.0.1",
"bcrypt": "^5.1.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"dotenv": "^16.0.3",
Expand Down Expand Up @@ -71,6 +72,7 @@
"lint-staged": "^13.1.0",
"prettier": "^2.3.2",
"source-map-support": "^0.5.20",
"sqlite3": "^5.1.6",
"supertest": "^6.1.3",
"ts-jest": "28.0.8",
"ts-loader": "^9.2.3",
Expand All @@ -93,6 +95,9 @@
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
"testEnvironment": "node",
"moduleNameMapper": {
"src/(.*)$": "<rootDir>/$1"
}
}
}
4 changes: 4 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@ import { TypeOrmModule } from "@nestjs/typeorm";

import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { AuthModule } from "./auth/auth.module";
import { dataSourceOptions } from "./config/data-source";
import { validate } from "./config/env.validation";
import { UserModule } from "./user/user.module";

@Module({
imports: [
ConfigModule.forRoot({
validate,
}),
TypeOrmModule.forRoot(dataSourceOptions),
UserModule,
AuthModule,
],
controllers: [AppController],
providers: [AppService],
Expand Down
79 changes: 79 additions & 0 deletions src/auth/auth.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { ConflictException } from "@nestjs/common";
import { Test, TestingModule } from "@nestjs/testing";
import { getRepositoryToken, TypeOrmModule } from "@nestjs/typeorm";
import { dataSourceJest } from "src/config/data-source";
import { UserEntity } from "src/user/entities/user.entity";
import { CreateUserRespose } from "src/user/resposes/create-user-respose";
import { UserService } from "src/user/user.service";
import { Repository } from "typeorm";

import { CreateUserDto } from "../user/dto/create-user.dto";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";

describe("AuthController", () => {
let authController: AuthController;
let authService: AuthService;
let userRepository: Repository<UserEntity>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [TypeOrmModule.forRoot(dataSourceJest)],
controllers: [AuthController],
providers: [
AuthService,
UserService,
{
provide: getRepositoryToken(UserEntity),
useValue: UserEntity, // 使用測試資料庫的 Repository
},
],
}).compile();

authController = module.get<AuthController>(AuthController);
authService = module.get<AuthService>(AuthService);
userRepository = module.get<Repository<UserEntity>>(
getRepositoryToken(UserEntity),
);
});
describe("create", () => {
it("應該會創建一個使用者,並返回 201 狀態碼", async () => {
const createUserDto: CreateUserDto = {
email: "[email protected]",
name: "displayname",
account: "account",
password: "Password@123",
};
const expectedResponse: CreateUserRespose = {
statusCode: 201,
message: "創建成功",
};
jest.spyOn(authService, "register").mockResolvedValue(expectedResponse);
const result = await authController.register(createUserDto);
expect(result).toEqual(expectedResponse);
});
it("應該會發生資料使用者重覆,並返回 409 狀態碼", async () => {
const createUserDto1: CreateUserDto = {
email: "[email protected]",
name: "displayname",
account: "account1",
password: "Password@123",
};
try {
await authService.register(createUserDto1);
await authService.register(createUserDto1);
} catch (error) {
expect(error).toBeInstanceOf(ConflictException);
expect(error.response).toEqual({
statusCode: 409,
message: ["email 已被註冊。", "account 已被註冊。"],
error: "Conflict",
});
}
});
});
afterEach(async () => {
if (userRepository && userRepository.clear) {
await userRepository.clear();
}
});
});
40 changes: 40 additions & 0 deletions src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Body, Controller, Post } from "@nestjs/common";
import {
ApiBadRequestResponse,
ApiConflictResponse,
ApiCreatedResponse,
ApiOperation,
ApiTags,
} from "@nestjs/swagger";
import { CreateUserDto } from "src/user/dto/create-user.dto";
import { CreateUserBadrequestError } from "src/user/exceptions/create-user-badrequest-error.exception";
import { CreateUserConflictError } from "src/user/exceptions/create-user-conflict-error.exception";
import { CreateUserRespose } from "src/user/resposes/create-user-respose";

import { AuthService } from "./auth.service";

@ApiTags("Auth")
@Controller("auth")
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post("register")
@ApiOperation({
summary: "使用者註冊",
description: "會檢查是否重複過的資料",
})
@ApiCreatedResponse({
description: "使用者創建成功",
type: CreateUserRespose,
})
@ApiConflictResponse({
description: "使用者資料重覆",
type: CreateUserConflictError,
})
@ApiBadRequestResponse({
description: "使用者格式不符",
type: CreateUserBadrequestError,
})
register(@Body() userDto: CreateUserDto) {
return this.authService.register(userDto);
}
}
14 changes: 14 additions & 0 deletions src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { UserEntity } from "src/user/entities/user.entity";
import { UserModule } from "src/user/user.module";

import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";

@Module({
imports: [UserModule, TypeOrmModule.forFeature([UserEntity])],
controllers: [AuthController],
providers: [AuthService],
})
export class AuthModule {}
129 changes: 129 additions & 0 deletions src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { ConflictException, HttpStatus } from "@nestjs/common";
import { Test, TestingModule } from "@nestjs/testing";
import { getRepositoryToken, TypeOrmModule } from "@nestjs/typeorm";
import { validate } from "class-validator";
import { dataSourceJest } from "src/config/data-source";
import { CreateUserDto } from "src/user/dto/create-user.dto";
import { UserEntity } from "src/user/entities/user.entity";
import { UserService } from "src/user/user.service";
import { Repository } from "typeorm";

import { AuthService } from "./auth.service";

describe("AuthService", () => {
let authService: AuthService;
let userRepository: Repository<UserEntity>;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [TypeOrmModule.forRoot(dataSourceJest)],
providers: [
AuthService,
UserService,
{
provide: getRepositoryToken(UserEntity),
useValue: UserEntity, // 使用測試資料庫的 Repository
},
],
}).compile();

authService = module.get<AuthService>(AuthService);
userRepository = module.get<Repository<UserEntity>>(
getRepositoryToken(UserEntity),
);
});

describe("createUser - Data", () => {
it("應該會創建 一個使用者", async () => {
const test_data: CreateUserDto = {
email: "[email protected]",
name: "displayname",
account: "account1",
password: "Password@123",
};
const user = await authService.register(test_data);

expect(user).toBeDefined();
expect(user.statusCode).toEqual(HttpStatus.CREATED);
expect(user.message).toEqual("創建成功");
});
it("應該會發生 email、account 已被註冊衝突", async () => {
const createUserDto1: CreateUserDto = {
email: "[email protected]",
name: "displayname",
account: "account",
password: "Password@123",
};
try {
await authService.register(createUserDto1);
await authService.register(createUserDto1);
} catch (error) {
expect(error).toBeInstanceOf(ConflictException);
expect(error.response).toEqual({
statusCode: 409,
message: ["email 已被註冊。", "account 已被註冊。"],
error: "Conflict",
});
}
});
it("應該會發生 email 已被註冊衝突", async () => {
const test_data1: CreateUserDto = {
email: "[email protected]",
name: "displayname",
account: "account1",
password: "Password@123",
};
const test_data2: CreateUserDto = {
email: "[email protected]",
name: "displayname",
account: "account2",
password: "Password@123",
};
const errors = await validate(test_data1);
expect(errors.length).toBe(0);
try {
await authService.register(test_data1);
await authService.register(test_data2);
} catch (error) {
expect(error).toBeInstanceOf(ConflictException);
expect(error.response).toEqual({
statusCode: 409,
message: ["email 已被註冊。"],
error: "Conflict",
});
}
});
it("應該會發生 account 已被註冊衝突", async () => {
const test_data1: CreateUserDto = {
email: "[email protected]",
name: "displayname",
account: "account",
password: "Password@123",
};
const test_data2: CreateUserDto = {
email: "[email protected]",
name: "displayname",
account: "account",
password: "Password@123",
};
const errors = await validate(test_data1);
expect(errors.length).toBe(0);
try {
await authService.register(test_data1);
await authService.register(test_data2);
} catch (error) {
expect(error).toBeInstanceOf(ConflictException);
expect(error.response).toEqual({
statusCode: 409,
message: ["account 已被註冊。"],
error: "Conflict",
});
}
});
});
afterEach(async () => {
if (userRepository && userRepository.clear) {
await userRepository.clear();
}
});
});
32 changes: 32 additions & 0 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ConflictException, Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { CreateUserDto } from "src/user/dto/create-user.dto";
import { UserEntity } from "src/user/entities/user.entity";
import { UserService } from "src/user/user.service";
import { Repository } from "typeorm";

@Injectable()
export class AuthService {
constructor(
private userService: UserService,
@InjectRepository(UserEntity)
private userRepository: Repository<UserEntity>,
) {}
async register(userDto: CreateUserDto) {
const existingUser = await this.userRepository.findOne({
where: [{ email: userDto.email }, { account: userDto.account }],
});

if (existingUser) {
const keys = ["email", "account"];
const conflictedAttributes = [];
keys.forEach(key => {
if (existingUser[key] === userDto[key]) {
conflictedAttributes.push(key + " 已被註冊。");
}
});
throw new ConflictException(conflictedAttributes);
}
return this.userService.create(userDto);
}
}
8 changes: 8 additions & 0 deletions src/config/data-source.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as dotenv from "dotenv";
import { UserEntity } from "src/user/entities/user.entity";
import { DataSource, DataSourceOptions } from "typeorm";

dotenv.config();
Expand All @@ -19,5 +20,12 @@ export const dataSourceOptions: DataSourceOptions = {
logging: false,
};

export const dataSourceJest: DataSourceOptions = {
type: "sqlite",
database: ":memory:",
entities: [UserEntity],
synchronize: true,
};

const dataSource = new DataSource(dataSourceOptions);
export default dataSource;
Loading

0 comments on commit 2f13ea2

Please sign in to comment.