Skip to content

Commit 2f13ea2

Browse files
authored
Feature/local register (#7) user register api & unit test
* 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
1 parent 55e5ba9 commit 2f13ea2

21 files changed

+1245
-18
lines changed

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"start:debug": "nest start --debug --watch",
1515
"start:prod": "node dist/main",
1616
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
17-
"test": "jest",
17+
"test": "jest --verbose",
1818
"test:watch": "jest --watch",
1919
"test:cov": "jest --coverage",
2020
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
@@ -39,6 +39,7 @@
3939
"@nestjs/platform-express": "^9.0.0",
4040
"@nestjs/swagger": "^6.1.4",
4141
"@nestjs/typeorm": "^9.0.1",
42+
"bcrypt": "^5.1.0",
4243
"class-transformer": "^0.5.1",
4344
"class-validator": "^0.14.0",
4445
"dotenv": "^16.0.3",
@@ -71,6 +72,7 @@
7172
"lint-staged": "^13.1.0",
7273
"prettier": "^2.3.2",
7374
"source-map-support": "^0.5.20",
75+
"sqlite3": "^5.1.6",
7476
"supertest": "^6.1.3",
7577
"ts-jest": "28.0.8",
7678
"ts-loader": "^9.2.3",
@@ -93,6 +95,9 @@
9395
"**/*.(t|j)s"
9496
],
9597
"coverageDirectory": "../coverage",
96-
"testEnvironment": "node"
98+
"testEnvironment": "node",
99+
"moduleNameMapper": {
100+
"src/(.*)$": "<rootDir>/$1"
101+
}
97102
}
98103
}

src/app.module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@ import { TypeOrmModule } from "@nestjs/typeorm";
44

55
import { AppController } from "./app.controller";
66
import { AppService } from "./app.service";
7+
import { AuthModule } from "./auth/auth.module";
78
import { dataSourceOptions } from "./config/data-source";
89
import { validate } from "./config/env.validation";
10+
import { UserModule } from "./user/user.module";
911

1012
@Module({
1113
imports: [
1214
ConfigModule.forRoot({
1315
validate,
1416
}),
1517
TypeOrmModule.forRoot(dataSourceOptions),
18+
UserModule,
19+
AuthModule,
1620
],
1721
controllers: [AppController],
1822
providers: [AppService],

src/auth/auth.controller.spec.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { ConflictException } from "@nestjs/common";
2+
import { Test, TestingModule } from "@nestjs/testing";
3+
import { getRepositoryToken, TypeOrmModule } from "@nestjs/typeorm";
4+
import { dataSourceJest } from "src/config/data-source";
5+
import { UserEntity } from "src/user/entities/user.entity";
6+
import { CreateUserRespose } from "src/user/resposes/create-user-respose";
7+
import { UserService } from "src/user/user.service";
8+
import { Repository } from "typeorm";
9+
10+
import { CreateUserDto } from "../user/dto/create-user.dto";
11+
import { AuthController } from "./auth.controller";
12+
import { AuthService } from "./auth.service";
13+
14+
describe("AuthController", () => {
15+
let authController: AuthController;
16+
let authService: AuthService;
17+
let userRepository: Repository<UserEntity>;
18+
beforeEach(async () => {
19+
const module: TestingModule = await Test.createTestingModule({
20+
imports: [TypeOrmModule.forRoot(dataSourceJest)],
21+
controllers: [AuthController],
22+
providers: [
23+
AuthService,
24+
UserService,
25+
{
26+
provide: getRepositoryToken(UserEntity),
27+
useValue: UserEntity, // 使用測試資料庫的 Repository
28+
},
29+
],
30+
}).compile();
31+
32+
authController = module.get<AuthController>(AuthController);
33+
authService = module.get<AuthService>(AuthService);
34+
userRepository = module.get<Repository<UserEntity>>(
35+
getRepositoryToken(UserEntity),
36+
);
37+
});
38+
describe("create", () => {
39+
it("應該會創建一個使用者,並返回 201 狀態碼", async () => {
40+
const createUserDto: CreateUserDto = {
41+
42+
name: "displayname",
43+
account: "account",
44+
password: "Password@123",
45+
};
46+
const expectedResponse: CreateUserRespose = {
47+
statusCode: 201,
48+
message: "創建成功",
49+
};
50+
jest.spyOn(authService, "register").mockResolvedValue(expectedResponse);
51+
const result = await authController.register(createUserDto);
52+
expect(result).toEqual(expectedResponse);
53+
});
54+
it("應該會發生資料使用者重覆,並返回 409 狀態碼", async () => {
55+
const createUserDto1: CreateUserDto = {
56+
57+
name: "displayname",
58+
account: "account1",
59+
password: "Password@123",
60+
};
61+
try {
62+
await authService.register(createUserDto1);
63+
await authService.register(createUserDto1);
64+
} catch (error) {
65+
expect(error).toBeInstanceOf(ConflictException);
66+
expect(error.response).toEqual({
67+
statusCode: 409,
68+
message: ["email 已被註冊。", "account 已被註冊。"],
69+
error: "Conflict",
70+
});
71+
}
72+
});
73+
});
74+
afterEach(async () => {
75+
if (userRepository && userRepository.clear) {
76+
await userRepository.clear();
77+
}
78+
});
79+
});

src/auth/auth.controller.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Body, Controller, Post } from "@nestjs/common";
2+
import {
3+
ApiBadRequestResponse,
4+
ApiConflictResponse,
5+
ApiCreatedResponse,
6+
ApiOperation,
7+
ApiTags,
8+
} from "@nestjs/swagger";
9+
import { CreateUserDto } from "src/user/dto/create-user.dto";
10+
import { CreateUserBadrequestError } from "src/user/exceptions/create-user-badrequest-error.exception";
11+
import { CreateUserConflictError } from "src/user/exceptions/create-user-conflict-error.exception";
12+
import { CreateUserRespose } from "src/user/resposes/create-user-respose";
13+
14+
import { AuthService } from "./auth.service";
15+
16+
@ApiTags("Auth")
17+
@Controller("auth")
18+
export class AuthController {
19+
constructor(private readonly authService: AuthService) {}
20+
@Post("register")
21+
@ApiOperation({
22+
summary: "使用者註冊",
23+
description: "會檢查是否重複過的資料",
24+
})
25+
@ApiCreatedResponse({
26+
description: "使用者創建成功",
27+
type: CreateUserRespose,
28+
})
29+
@ApiConflictResponse({
30+
description: "使用者資料重覆",
31+
type: CreateUserConflictError,
32+
})
33+
@ApiBadRequestResponse({
34+
description: "使用者格式不符",
35+
type: CreateUserBadrequestError,
36+
})
37+
register(@Body() userDto: CreateUserDto) {
38+
return this.authService.register(userDto);
39+
}
40+
}

src/auth/auth.module.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Module } from "@nestjs/common";
2+
import { TypeOrmModule } from "@nestjs/typeorm";
3+
import { UserEntity } from "src/user/entities/user.entity";
4+
import { UserModule } from "src/user/user.module";
5+
6+
import { AuthController } from "./auth.controller";
7+
import { AuthService } from "./auth.service";
8+
9+
@Module({
10+
imports: [UserModule, TypeOrmModule.forFeature([UserEntity])],
11+
controllers: [AuthController],
12+
providers: [AuthService],
13+
})
14+
export class AuthModule {}

src/auth/auth.service.spec.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { ConflictException, HttpStatus } from "@nestjs/common";
2+
import { Test, TestingModule } from "@nestjs/testing";
3+
import { getRepositoryToken, TypeOrmModule } from "@nestjs/typeorm";
4+
import { validate } from "class-validator";
5+
import { dataSourceJest } from "src/config/data-source";
6+
import { CreateUserDto } from "src/user/dto/create-user.dto";
7+
import { UserEntity } from "src/user/entities/user.entity";
8+
import { UserService } from "src/user/user.service";
9+
import { Repository } from "typeorm";
10+
11+
import { AuthService } from "./auth.service";
12+
13+
describe("AuthService", () => {
14+
let authService: AuthService;
15+
let userRepository: Repository<UserEntity>;
16+
17+
beforeEach(async () => {
18+
const module: TestingModule = await Test.createTestingModule({
19+
imports: [TypeOrmModule.forRoot(dataSourceJest)],
20+
providers: [
21+
AuthService,
22+
UserService,
23+
{
24+
provide: getRepositoryToken(UserEntity),
25+
useValue: UserEntity, // 使用測試資料庫的 Repository
26+
},
27+
],
28+
}).compile();
29+
30+
authService = module.get<AuthService>(AuthService);
31+
userRepository = module.get<Repository<UserEntity>>(
32+
getRepositoryToken(UserEntity),
33+
);
34+
});
35+
36+
describe("createUser - Data", () => {
37+
it("應該會創建 一個使用者", async () => {
38+
const test_data: CreateUserDto = {
39+
40+
name: "displayname",
41+
account: "account1",
42+
password: "Password@123",
43+
};
44+
const user = await authService.register(test_data);
45+
46+
expect(user).toBeDefined();
47+
expect(user.statusCode).toEqual(HttpStatus.CREATED);
48+
expect(user.message).toEqual("創建成功");
49+
});
50+
it("應該會發生 email、account 已被註冊衝突", async () => {
51+
const createUserDto1: CreateUserDto = {
52+
53+
name: "displayname",
54+
account: "account",
55+
password: "Password@123",
56+
};
57+
try {
58+
await authService.register(createUserDto1);
59+
await authService.register(createUserDto1);
60+
} catch (error) {
61+
expect(error).toBeInstanceOf(ConflictException);
62+
expect(error.response).toEqual({
63+
statusCode: 409,
64+
message: ["email 已被註冊。", "account 已被註冊。"],
65+
error: "Conflict",
66+
});
67+
}
68+
});
69+
it("應該會發生 email 已被註冊衝突", async () => {
70+
const test_data1: CreateUserDto = {
71+
72+
name: "displayname",
73+
account: "account1",
74+
password: "Password@123",
75+
};
76+
const test_data2: CreateUserDto = {
77+
78+
name: "displayname",
79+
account: "account2",
80+
password: "Password@123",
81+
};
82+
const errors = await validate(test_data1);
83+
expect(errors.length).toBe(0);
84+
try {
85+
await authService.register(test_data1);
86+
await authService.register(test_data2);
87+
} catch (error) {
88+
expect(error).toBeInstanceOf(ConflictException);
89+
expect(error.response).toEqual({
90+
statusCode: 409,
91+
message: ["email 已被註冊。"],
92+
error: "Conflict",
93+
});
94+
}
95+
});
96+
it("應該會發生 account 已被註冊衝突", async () => {
97+
const test_data1: CreateUserDto = {
98+
99+
name: "displayname",
100+
account: "account",
101+
password: "Password@123",
102+
};
103+
const test_data2: CreateUserDto = {
104+
105+
name: "displayname",
106+
account: "account",
107+
password: "Password@123",
108+
};
109+
const errors = await validate(test_data1);
110+
expect(errors.length).toBe(0);
111+
try {
112+
await authService.register(test_data1);
113+
await authService.register(test_data2);
114+
} catch (error) {
115+
expect(error).toBeInstanceOf(ConflictException);
116+
expect(error.response).toEqual({
117+
statusCode: 409,
118+
message: ["account 已被註冊。"],
119+
error: "Conflict",
120+
});
121+
}
122+
});
123+
});
124+
afterEach(async () => {
125+
if (userRepository && userRepository.clear) {
126+
await userRepository.clear();
127+
}
128+
});
129+
});

src/auth/auth.service.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { ConflictException, Injectable } from "@nestjs/common";
2+
import { InjectRepository } from "@nestjs/typeorm";
3+
import { CreateUserDto } from "src/user/dto/create-user.dto";
4+
import { UserEntity } from "src/user/entities/user.entity";
5+
import { UserService } from "src/user/user.service";
6+
import { Repository } from "typeorm";
7+
8+
@Injectable()
9+
export class AuthService {
10+
constructor(
11+
private userService: UserService,
12+
@InjectRepository(UserEntity)
13+
private userRepository: Repository<UserEntity>,
14+
) {}
15+
async register(userDto: CreateUserDto) {
16+
const existingUser = await this.userRepository.findOne({
17+
where: [{ email: userDto.email }, { account: userDto.account }],
18+
});
19+
20+
if (existingUser) {
21+
const keys = ["email", "account"];
22+
const conflictedAttributes = [];
23+
keys.forEach(key => {
24+
if (existingUser[key] === userDto[key]) {
25+
conflictedAttributes.push(key + " 已被註冊。");
26+
}
27+
});
28+
throw new ConflictException(conflictedAttributes);
29+
}
30+
return this.userService.create(userDto);
31+
}
32+
}

src/config/data-source.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as dotenv from "dotenv";
2+
import { UserEntity } from "src/user/entities/user.entity";
23
import { DataSource, DataSourceOptions } from "typeorm";
34

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

23+
export const dataSourceJest: DataSourceOptions = {
24+
type: "sqlite",
25+
database: ":memory:",
26+
entities: [UserEntity],
27+
synchronize: true,
28+
};
29+
2230
const dataSource = new DataSource(dataSourceOptions);
2331
export default dataSource;

0 commit comments

Comments
 (0)