Commit fa276d02 authored by ThinhNC's avatar ThinhNC

docs: implement system architecture, API specs, deployment guide, enhance demo...

docs: implement system architecture, API specs, deployment guide, enhance demo seed, and setup integration tests
parent 4af76950
...@@ -51,12 +51,13 @@ File này chỉ lưu sự thật và quyết định dài hạn giúp các phiê ...@@ -51,12 +51,13 @@ File này chỉ lưu sự thật và quyết định dài hạn giúp các phiê
- LockService cung cấp phân phối khoá (Redis hoặc memory fallback) nhằm ngăn chặn tranh chấp chạy song song của worker nền trong môi trường production. - LockService cung cấp phân phối khoá (Redis hoặc memory fallback) nhằm ngăn chặn tranh chấp chạy song song của worker nền trong môi trường production.
- Rate Limiting tổng thể được xây dựng để sử dụng Redis (kết hợp memory fallback an toàn, có cơ chế tự giải phóng dữ liệu tránh rò rỉ bộ nhớ). - Rate Limiting tổng thể được xây dựng để sử dụng Redis (kết hợp memory fallback an toàn, có cơ chế tự giải phóng dữ liệu tránh rò rỉ bộ nhớ).
- Hệ thống log sử dụng LoggerService, đầu ra JSON ở production và text màu ở development, hỗ trợ ẩn thông tin nhạy cảm. - Hệ thống log sử dụng LoggerService, đầu ra JSON ở production và text màu ở development, hỗ trợ ẩn thông tin nhạy cảm.
- Môi trường production được container hóa bằng Dockerfile (multi-stage) chạy với user phi quản trị và docker-compose.yml có thiết lập kiểm tra sức khoẻ (healthcheck) cho Postgres và Redis. - Môi trường production được container hóa bằng Dockerfile (multi-stage) chạy với user phi quản trị và docker-compose.yml có thiết hành kiểm tra sức khoẻ (healthcheck) cho Postgres và Redis.
- Script seed (prisma/seed.ts) được mở rộng để tự động tạo dữ liệu mẫu demo phong phú (Wallets, Transactions, Budgets, SavingGoals, Contributions, Notifications, Reminders) cho tài khoản user@finwise.local.
## Trạng thái đã biết ## Trạng thái đã biết
- Chưa có test script hoặc test suite trong `package.json`. - Đã thiết lập khung kiểm thử tích hợp (integration tests) bằng Jest và Supertest, chạy kiểm thử qua lệnh pnpm test sử dụng cấu hình môi trường test cô lập (REDIS_ENABLED=false để chạy in-memory cache).
- Hệ thống linting đã được cấu hình qua `eslint.config.mjs` (flat config) và chạy sạch sẽ khi gọi `pnpm run lint`. - Hệ thống linting đã được cấu hình qua eslint.config.mjs (flat config, bỏ qua các tệp test & configs liên quan) và chạy sạch sẽ khi gọi pnpm run lint.
- `env.config.ts` dùng port fallback `8888`, còn `.env.example` dùng `7777`; README - `env.config.ts` dùng port fallback `8888`, còn `.env.example` dùng `7777`; README
ghi rõ cả hai và dùng `7777` cho hướng dẫn chạy theo file env mẫu. ghi rõ cả hai và dùng `7777` cho hướng dẫn chạy theo file env mẫu.
- Wallet, Category, Transaction và Budget đã có API theo ownership; Category đồng - Wallet, Category, Transaction và Budget đã có API theo ownership; Category đồng
......
...@@ -2,7 +2,16 @@ ...@@ -2,7 +2,16 @@
Backend API cho **FinWise - Sổ tay Chi tiêu & Báo cáo Tài chính**, phục vụ Zalo Mini App. Backend API cho **FinWise - Sổ tay Chi tiêu & Báo cáo Tài chính**, phục vụ Zalo Mini App.
API hiện hỗ trợ: ## 📚 Tài liệu Kỹ thuật Chuyên sâu
Để tìm hiểu chi tiết về hệ thống, vui lòng xem các tài liệu chuyên sâu dưới đây:
* [Kiến trúc Hệ thống & Sơ đồ ERD](./docs/architecture.md): Mô tả mô hình phân tầng, Mermaid ERD chi tiết của 14 bảng, và các cơ chế Cache, Lock, Rate Limiting, Logging.
* [Tài liệu API chi tiết](./docs/api.md): Mô tả định dạng request/response, xác thực qua token JWT, cookie HTTP-only và danh sách toàn bộ endpoints.
* [Hướng dẫn Cài đặt & Triển khai](./docs/deployment.md): Hướng dẫn thiết lập môi trường development cục bộ, nạp seed data và vận hành container hoá bằng Docker Compose (healthcheck & non-root user) hoặc PM2 Cluster.
---
## Tính năng chính
- Đăng ký, xác thực email, đăng nhập JWT và quản lý phiên đăng nhập. - Đăng ký, xác thực email, đăng nhập JWT và quản lý phiên đăng nhập.
- Quản lý hồ sơ, mật khẩu và khôi phục mật khẩu qua email. - Quản lý hồ sơ, mật khẩu và khôi phục mật khẩu qua email.
...@@ -289,7 +298,15 @@ route -> validation -> controller -> service -> repository -> Prisma/PostgreSQL ...@@ -289,7 +298,15 @@ route -> validation -> controller -> service -> repository -> Prisma/PostgreSQL
| `pnpm run db:migrate` | Tạo/apply migration trong development | | `pnpm run db:migrate` | Tạo/apply migration trong development |
| `pnpm run db:migrate:deploy` | Apply migration hiện có | | `pnpm run db:migrate:deploy` | Apply migration hiện có |
| `pnpm run db:migrate:status` | Xem trạng thái migration | | `pnpm run db:migrate:status` | Xem trạng thái migration |
| `pnpm run db:seed` | Seed dữ liệu development | | `pnpm run db:seed` | Seed dữ liệu development & demo data |
| `pnpm test` | Chạy bộ kiểm thử tự động với Jest |
| `pnpm run test:cov` | Chạy test suite và báo cáo độ phủ (cov)|
| `pnpm format` | Format repository bằng Prettier | | `pnpm format` | Format repository bằng Prettier |
Repository hiện chưa có test suite. Script `lint` đã được khai báo nhưng chưa có ESLint config, vì vậy verification khả dụng hiện tại là Prisma validation và TypeScript build. ## Kiểm thử tự động (Testing)
Hệ thống đã được thiết lập bộ kiểm thử tích hợp (integration tests) bằng **Jest****Supertest**:
- Thư mục kiểm thử: `tests/`
- Tệp cấu hình: `jest.config.ts``tests/setup.ts`
- Các test suites khả dụng: kiểm tra tính sẵn sàng biên HTTP (`health.test.ts`), luồng đăng ký, đăng nhập và xác thực token kép (`auth.test.ts`).
- Chạy kiểm tra: `pnpm test`
# Hướng dẫn và Mô tả Chi tiết API FinWise
FinWise API được xây dựng theo chuẩn RESTful, trả về dữ liệu định dạng JSON nhất quán và hỗ trợ tích hợp trực quan qua Swagger UI.
---
## 1. Cổng Thông tin Tài liệu API (Swagger UI)
* **Đường dẫn truy cập cục bộ**: `http://localhost:7777/api/docs`
* **Giao diện**: Swagger UI được cấu hình với giao diện tối tối ưu (dark mode), cho phép thử nghiệm trực tiếp các tham số request và xem chi tiết cấu trúc JSON Schema cho từng API.
---
## 2. Chuẩn Giao tiếp & Xác thực
### 2.1 Cấu trúc Phản hồi Chuẩn (Response Schema)
#### Phản hồi Thành công (Success Response):
Mọi response thành công đều trả về HTTP Status `2xx` và có thuộc tính `success: true`:
```json
{
"success": true,
"data": {
"id": "c30172bf-d6ff-4340-9a84-0a3ffb1d9bf8",
"name": "Ví tiền mặt",
"balance": "4500000.00",
"currency": "VND"
}
}
```
#### Phản hồi Thất bại (Error Response):
Mọi response lỗi đều trả về HTTP Status tương ứng (`4xx`, `5xx`) và có cấu trúc:
```json
{
"success": false,
"message": "Thông điệp mô tả lỗi chi tiết cho client",
"code": "ERROR_CODE_NGHIEP_VU"
}
```
*Các mã lỗi nghiệp vụ (`code`) thông dụng*: `INVALID_CREDENTIALS`, `USER_INACTIVE`, `UNAUTHORIZED`, `TOKEN_EXPIRED`, `TOKEN_INVALID`, `NOT_FOUND`, `DUPLICATE_ENTRY`, `RATE_LIMIT_EXCEEDED`, `SERVICE_UNAVAILABLE`.
### 2.2 Xác thực qua Token JWT
Hệ thống sử dụng cơ chế token kép (Access Token và Refresh Token):
1. **Access Token**: Truyền qua Header HTTP:
```http
Authorization: Bearer <Your_Access_Token>
```
*Hoặc* hệ thống sẽ tự động đọc từ Cookie HTTP-only `accessToken` nếu có.
2. **Refresh Token**: Được lưu tự động trong Cookie HTTP-only `refreshToken` khi đăng nhập thành công. Để làm mới cặp token, gọi API `/auth/refresh`.
---
## 3. Danh sách Endpoints Chính
Tất cả các endpoint bên dưới có tiền tố (prefix) mặc định: `/api/v1`
### 3.1 Hệ thống & Xác thực (Auth)
* `GET /health` [Public] - Kiểm tra sức khỏe của API, database, cache và đo độ trễ.
* `POST /auth/register` [Public] - Đăng ký tài khoản (kích hoạt qua token email).
* `GET /auth/verify-email?token=<token>` [Public] - Xác thực email để active tài khoản.
* `POST /auth/login` [Public] - Đăng nhập tài khoản, nhận token và lưu cookie.
* `GET /auth/me` [Bearer] - Lấy thông tin tài khoản hiện tại.
* `POST /auth/refresh` [Public] - Dùng Refresh Token để làm mới cặp Access/Refresh Token.
* `POST /auth/logout` [Public] - Đăng xuất và thu hồi Refresh Token trong DB.
* `PUT /auth/profile` [Bearer] - Cập nhật thông tin cá nhân (họ tên, số điện thoại, avatar).
* `PUT /auth/password` [Bearer] - Đổi mật khẩu tài khoản.
* `POST /auth/forgot-password` [Public] - Gửi email khôi phục mật khẩu.
* `POST /auth/reset-password` [Public] - Đặt lại mật khẩu mới sử dụng token.
* `GET /auth/sessions` [Bearer] - Danh sách các thiết bị/phiên đăng nhập hoạt động.
* `DELETE /auth/sessions/:id` [Bearer] - Đăng xuất từ xa một phiên thiết bị cụ thể.
### 3.2 Quản trị Người dùng (Users) - Chỉ dành cho vai trò ADMIN
* `GET /users` - Lấy danh sách người dùng kèm phân trang, tìm kiếm và lọc trạng thái.
* `POST /users` - Admin tạo tài khoản người dùng trực tiếp.
* `PUT /users/:id` - Admin cập nhật trạng thái hoạt động (`isActive`) hoặc vai trò (`roleId`).
* `DELETE /users/:id` - Admin thực hiện xóa mềm (Soft-delete) tài khoản người dùng.
### 3.3 Quản lý Ví (Wallets) - Theo quyền sở hữu (Ownership)
* `GET /wallets` [Bearer] - Lấy danh sách ví hoạt động (kèm số dư, icon, màu sắc).
* `POST /wallets` [Bearer] - Tạo ví mới (hỗ trợ các loại tiền tệ VND, USD,...).
* `GET /wallets/:id` [Bearer] - Xem chi tiết một ví.
* `PUT /wallets/:id` [Bearer] - Cập nhật thông tin ví.
* `PATCH /wallets/:id/default` [Bearer] - Thiết lập một ví làm ví mặc định của hệ thống.
* `DELETE /wallets/:id` [Bearer] - Lưu trữ (archive) ví thay vì xóa vật lý để bảo toàn lịch sử giao dịch.
### 3.4 Danh mục Thu Chi (Categories)
* `GET /categories` [Bearer] - Lấy danh sách category bao gồm danh mục hệ thống (isSystem=true) và danh mục riêng của người dùng.
* `GET /categories/tree` [Bearer] - Lấy danh sách danh mục theo cấu trúc hình cây (cha - con).
* `POST /categories` [Bearer] - Tạo danh mục con hoặc danh mục riêng mới.
* `DELETE /categories/:id` [Bearer] - Lưu trữ (archive) danh mục riêng.
### 3.5 Giao dịch (Transactions) - Tự động đồng bộ số dư ví
* `GET /transactions` [Bearer] - Danh sách giao dịch có bộ lọc mạnh mẽ theo ví, danh mục, khoảng thời gian, loại thu/chi và phân trang.
* `POST /transactions` [Bearer] - Tạo giao dịch thu/chi (tự động cộng/trừ số dư ví liên quan dưới database transaction Serializable).
* `PUT /transactions/:id` [Bearer] - Cập nhật giao dịch (tự động tính toán lại và hoàn tác/cập nhật số dư ví cũ và mới tương ứng).
* `DELETE /transactions/:id` [Bearer] - Xóa giao dịch (tự động hoàn trả số dư ví về trạng thái trước giao dịch).
* `PUT /transactions/:id/receipt` [Bearer] - Tải lên ảnh hóa đơn (`receipt`) dạng multipart-form (hỗ trợ JPEG, PNG, WebP, PDF tối đa 5MB).
* `GET /transactions/:id/receipt` [Bearer] - Xem/Tải xuống tệp hóa đơn đã upload bảo mật.
### 3.6 Ngân sách chi tiêu (Budgets)
* `GET /budgets` [Bearer] - Danh sách ngân sách kèm tiến độ sử dụng tính theo thời gian thực từ giao dịch chi tiêu tương ứng.
* `POST /budgets` [Bearer] - Tạo ngân sách tổng quát (`OVERALL`) hoặc theo danh mục chi tiêu (`CATEGORY`) với ngưỡng cảnh báo tùy chọn.
### 3.7 Mục tiêu tiết kiệm (Saving Goals)
* `GET /saving-goals` [Bearer] - Danh sách mục tiêu tiết kiệm và tiến trình đạt được (%).
* `POST /saving-goals` [Bearer] - Tạo mục tiêu mới.
* `POST /saving-goals/:id/contributions` [Bearer] - Gửi tiền tích lũy vào mục tiêu tiết kiệm (hệ thống tự động cập nhật tiến trình và trạng thái hoàn thành).
### 3.8 Báo cáo tài chính (Reports) - Đọc dữ liệu nhanh có Cache
* `GET /reports/overview` [Bearer] - Báo cáo tổng quan số dư, tổng thu, tổng chi và dòng tiền ròng.
* `GET /reports/cash-flow` [Bearer] - Chuỗi dữ liệu dòng tiền theo ngày/tuần/tháng để vẽ biểu đồ đường.
* `GET /reports/category-distribution` [Bearer] - Cơ cấu chi tiêu phân chia theo danh mục để vẽ biểu đồ tròn.
* `GET /reports/budget-performance` [Bearer] - So sánh ngân sách và thực tế chi tiêu.
### 3.9 Trợ lý Tài chính AI (AI Assistant)
* `POST /ai-assistant/chat` [Bearer] - Hỏi đáp tài chính cá nhân với trợ lý AI dựa trên dữ liệu thu chi thực tế của người dùng.
* `POST /ai-assistant/classify` [Bearer] - Phân tích văn bản giao dịch tự do để đề xuất danh mục thích hợp.
* `POST /ai-assistant/ocr` [Bearer] - Phân tích ảnh hóa đơn tải lên để tự động bóc tách số tiền, danh mục, ngày tháng.
This diff is collapsed.
# Hướng dẫn Cài đặt và Triển khai Hệ thống FinWise Backend
Tài liệu này cung cấp hướng dẫn cài đặt từ môi trường phát triển (Development) cục bộ cho đến môi trường vận hành thực tế (Production) có container hoá.
---
## 1. Yêu cầu Hệ thống tối thiểu
* **Node.js**: Phiên bản 18.x trở lên.
* **Package Manager**: `pnpm` phiên bản 9.x trở lên.
* **Database**: PostgreSQL 15.x trở lên.
* **Cache & Session**: Redis 7.x trở lên.
* **Docker & Docker Compose**: Nếu triển khai bằng Container.
---
## 2. Hướng dẫn Triển khai cục bộ (Local Development)
### Bước 2.1: Tải mã nguồn và Cài đặt thư viện phụ thuộc
Sử dụng `pnpm` để cài đặt dependencies theo chuẩn cấu hình `package.json`:
```bash
pnpm install
```
### Bước 2.2: Cấu hình biến môi trường
1. Sao chép file cấu hình mẫu:
```bash
cp .env.example .env
```
2. Mở file `.env` và cập nhật thông số kết nối Database, Redis và JWT:
```env
NODE_ENV=development
PORT=7777
# Kết nối PostgreSQL
DATABASE_URL="postgresql://postgres:password@localhost:5432/datafinwise?schema=public"
# Cấu hình Token bảo mật
JWT_ACCESS_SECRET="your_strong_access_secret_key"
JWT_REFRESH_SECRET="your_strong_refresh_secret_key"
JWT_ACCESS_EXPIRES_IN=1d
JWT_REFRESH_EXPIRES_IN=7d
# Kết nối Cache Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_ENABLED=true
# Dịch vụ AI (Trợ lý Tài chính)
AI_PROVIDER=gemini
GEMINI_API_KEYS="key1,key2" # Danh sách khóa xoay vòng ngăn lỗi quota limit
```
### Bước 2.3: Chuẩn bị Cơ sở dữ liệu (Prisma setup)
Chạy tuần tự các lệnh sau để kiểm tra cấu trúc schema, sinh kiểu (client types) và cập nhật cơ sở dữ liệu:
```bash
# Validate cấu trúc Prisma schema
pnpm exec prisma validate
# Sinh mã Prisma Client tương thích
pnpm run prisma:generate
# Triển khai các file migration và cập nhật cấu trúc database
pnpm run db:migrate:deploy
# Nạp dữ liệu mẫu demo phong phú (Ví, giao dịch, budget, saving goals mẫu)
pnpm run db:seed
```
### Bước 2.4: Khởi động Server phát triển
```bash
pnpm dev
```
Hệ thống sẽ chạy tại `http://localhost:7777`.
---
## 3. Triển khai Production sử dụng Docker (Khuyến nghị)
FinWise cung cấp file cấu hình Docker tối ưu bảo mật chạy dưới quyền **non-root user** để ngăn chặn leo thang đặc quyền bảo mật.
### Bước 3.1: Build Container Image
Dockerfile multi-stage giúp giảm tối đa dung lượng image và loại bỏ source code TypeScript thừa ở runtime:
```bash
docker build -t finwise-backend:latest .
```
### Bước 3.2: Chạy toàn bộ Stack dịch vụ bằng Docker Compose
File `docker-compose.yml` định nghĩa đầy đủ 3 services chính: `app` (Node.js API), `postgres` (Database), `redis` (Cache).
Đặc biệt, hệ thống sử dụng **Healthchecks** tích hợp để đảm bảo các dịch vụ hạ tầng sẵn sàng trước khi nạp ứng dụng.
Để khởi động toàn bộ hệ thống ở chế độ nền (detached mode):
```bash
docker compose up -d
```
Để theo dõi log hoạt động:
```bash
docker compose logs -f
```
Để dừng hệ thống và bảo lưu dữ liệu (Named volumes):
```bash
docker compose down
```
---
## 4. Triển khai bằng PM2 (Môi trường Linux VPS thông thường)
Nếu không sử dụng Docker trên máy chủ, sử dụng công cụ quản lý tiến trình **PM2** để chạy ngầm và tự động khởi động lại ứng dụng khi gặp sự cố crash.
### Bước 4.1: Cài đặt PM2 toàn cục
```bash
npm install -g pm2
```
### Bước 4.2: Build mã nguồn TypeScript thành Javascript
```bash
pnpm build
```
### Bước 4.3: Khởi động ứng dụng bằng PM2
Tạo file cấu hình `ecosystem.config.js` ở thư mục gốc:
```javascript
module.exports = {
apps: [
{
name: 'finwise-backend',
script: 'dist/server.js',
instances: 'max', // Chạy chế độ Cluster tận dụng tối đa số nhân CPU
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
},
},
],
};
```
Khởi động ứng dụng:
```bash
pm2 start ecosystem.config.js
```
Kiểm tra trạng thái các instances:
```bash
pm2 status
```
...@@ -9,6 +9,8 @@ export default tseslint.config( ...@@ -9,6 +9,8 @@ export default tseslint.config(
'scripts/**/*', 'scripts/**/*',
'prisma/**/*', 'prisma/**/*',
'eslint.config.mjs', 'eslint.config.mjs',
'jest.config.ts',
'tests/**/*',
], ],
}, },
js.configs.recommended, js.configs.recommended,
......
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src', '<rootDir>/tests'],
testMatch: ['**/*.spec.ts', '**/*.test.ts'],
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
verbose: true,
forceExit: true,
clearMocks: true,
resetMocks: true,
restoreMocks: true,
};
export default config;
...@@ -17,7 +17,10 @@ ...@@ -17,7 +17,10 @@
"db:migrate:status": "node scripts/prisma-run.js migrate status", "db:migrate:status": "node scripts/prisma-run.js migrate status",
"db:seed": "node scripts/prisma-run.js db seed -- --tsx prisma/seed.ts", "db:seed": "node scripts/prisma-run.js db seed -- --tsx prisma/seed.ts",
"lint": "eslint .", "lint": "eslint .",
"format": "prettier --write ." "format": "prettier --write .",
"test": "jest --runInBand",
"test:watch": "jest --watch --runInBand",
"test:cov": "jest --coverage --runInBand"
}, },
"dependencies": { "dependencies": {
"@prisma/client": "^5.22.0", "@prisma/client": "^5.22.0",
...@@ -41,15 +44,20 @@ ...@@ -41,15 +44,20 @@
"@types/cookie-parser": "^1.4.10", "@types/cookie-parser": "^1.4.10",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/jest": "^30.0.0",
"@types/jsonwebtoken": "^9.0.7", "@types/jsonwebtoken": "^9.0.7",
"@types/morgan": "^1.9.9", "@types/morgan": "^1.9.9",
"@types/multer": "^2.2.0", "@types/multer": "^2.2.0",
"@types/node": "^22.10.2", "@types/node": "^22.10.2",
"@types/nodemailer": "^8.0.1", "@types/nodemailer": "^8.0.1",
"@types/supertest": "^7.2.1",
"@types/swagger-ui-express": "^4.1.8", "@types/swagger-ui-express": "^4.1.8",
"eslint": "^9.17.0", "eslint": "^9.17.0",
"jest": "^30.4.2",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"prisma": "^5.22.0", "prisma": "^5.22.0",
"supertest": "^7.2.2",
"ts-jest": "^29.4.12",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",
"tsx": "^4.19.2", "tsx": "^4.19.2",
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
import request from 'supertest';
import app from '../src/app';
import { prisma } from '../src/database/prisma.client';
describe('Auth Integration Tests', () => {
const testUser = {
email: 'register-test@gmail.com',
password: 'Password@123456',
fullName: 'Test Register User',
};
afterAll(async () => {
// Dọn dẹp dữ liệu kiểm thử
await prisma.verificationToken.deleteMany({
where: {
user: {
email: testUser.email,
},
},
});
await prisma.refreshToken.deleteMany({
where: {
user: {
email: testUser.email,
},
},
});
await prisma.userDevice.deleteMany({
where: {
user: {
email: testUser.email,
},
},
});
await prisma.user.deleteMany({
where: {
email: testUser.email,
},
});
await prisma.$disconnect();
});
let verificationToken = '';
let accessTokenCookie = '';
let accessTokenHeader = '';
it('should register a new user successfully', async () => {
const res = await request(app)
.post('/api/v1/auth/register')
.send(testUser);
expect(res.status).toBe(201);
expect(res.body).toHaveProperty('success', true);
expect(res.body).toHaveProperty('message');
// Lấy token từ database để verify
const dbUser = await prisma.user.findUnique({
where: { email: testUser.email },
include: { verificationTokens: true },
});
expect(dbUser).toBeDefined();
expect(dbUser?.isActive).toBe(false);
expect(dbUser?.verificationTokens.length).toBe(1);
verificationToken = dbUser?.verificationTokens[0].token || '';
});
it('should not allow login with inactive account', async () => {
const res = await request(app)
.post('/api/v1/auth/login')
.send({
email: testUser.email,
password: testUser.password,
});
expect(res.status).toBe(403);
expect(res.body).toHaveProperty('success', false);
expect(res.body.code).toBe('USER_INACTIVE');
});
it('should verify email successfully', async () => {
const res = await request(app)
.get(`/api/v1/auth/verify-email?token=${verificationToken}`);
expect(res.status).toBe(200);
expect(res.body).toHaveProperty('success', true);
expect(res.body).toHaveProperty('message');
const dbUser = await prisma.user.findUnique({
where: { email: testUser.email },
});
expect(dbUser?.isActive).toBe(true);
});
it('should login successfully and set cookies', async () => {
const res = await request(app)
.post('/api/v1/auth/login')
.send({
email: testUser.email,
password: testUser.password,
});
expect(res.status).toBe(200);
expect(res.body).toHaveProperty('success', true);
expect(res.body.data).toHaveProperty('user');
expect(res.body.data.user).toHaveProperty('email', testUser.email);
// Lấy cookie
const cookies = (res.headers['set-cookie'] || []) as string[];
expect(cookies).toBeDefined();
const accessTokenMatch = cookies.find(c => c.startsWith('accessToken='));
expect(accessTokenMatch).toBeDefined();
// Trích xuất JWT token phục vụ test gọi API bằng header
const token = accessTokenMatch?.split(';')[0].split('=')[1];
expect(token).toBeDefined();
accessTokenHeader = token || '';
// Giữ nguyên mảng cookie để gọi API qua cookie
accessTokenCookie = cookies.join('; ');
});
it('should get current user profile using Bearer token header', async () => {
const res = await request(app)
.get('/api/v1/auth/me')
.set('Authorization', `Bearer ${accessTokenHeader}`);
expect(res.status).toBe(200);
expect(res.body).toHaveProperty('success', true);
expect(res.body.data).toHaveProperty('email', testUser.email);
});
it('should get current user profile using Cookie', async () => {
const res = await request(app)
.get('/api/v1/auth/me')
.set('Cookie', accessTokenCookie);
expect(res.status).toBe(200);
expect(res.body).toHaveProperty('success', true);
expect(res.body.data).toHaveProperty('email', testUser.email);
});
it('should logout successfully and clear cookies', async () => {
// Trích xuất refresh token từ DB để gửi kèm body nếu logout yêu cầu (hoặc qua cookie)
const dbUser = await prisma.user.findUnique({
where: { email: testUser.email },
include: { refreshTokens: true },
});
const refreshToken = dbUser?.refreshTokens[0]?.token || '';
const res = await request(app)
.post('/api/v1/auth/logout')
.set('Cookie', accessTokenCookie)
.send({ refreshToken });
expect(res.status).toBe(200);
expect(res.body).toHaveProperty('success', true);
expect(res.body).toHaveProperty('message');
// Kiểm tra xem refresh token trong DB đã bị xoá chưa
const tokensCount = await prisma.refreshToken.count({
where: {
userId: dbUser?.id,
},
});
expect(tokensCount).toBe(0);
});
});
import request from 'supertest';
import app from '../src/app';
import { prisma } from '../src/database/prisma.client';
describe('GET /api/v1/health', () => {
it('should return 200 and status ok if database is up', async () => {
// Đảm bảo prisma hoạt động
jest.spyOn(prisma, '$queryRaw').mockResolvedValueOnce([1]);
const res = await request(app).get('/api/v1/health');
expect(res.status).toBe(200);
expect(res.body).toHaveProperty('success', true);
expect(res.body).toHaveProperty('status', 'ok');
expect(res.body).toHaveProperty('database');
expect(res.body.database).toHaveProperty('status', 'up');
});
it('should return 503 and status error if database is down', async () => {
// Giả lập lỗi truy vấn database
jest.spyOn(prisma, '$queryRaw').mockRejectedValueOnce(new Error('Connection failed'));
const res = await request(app).get('/api/v1/health');
expect(res.status).toBe(503);
expect(res.body).toHaveProperty('success', false);
expect(res.body).toHaveProperty('status', 'error');
expect(res.body.database).toHaveProperty('status', 'down');
});
});
// Cấu hình môi trường chạy test
process.env.NODE_ENV = 'test';
process.env.PORT = '8889';
process.env.JWT_ACCESS_SECRET = 'test_access_secret_key_123456789_xyz';
process.env.JWT_REFRESH_SECRET = 'test_refresh_secret_key_123456789_xyz';
process.env.REDIS_ENABLED = 'false';
process.env.NOTIFICATION_WORKER_ENABLED = 'false';
// Mock MailService để tránh gửi mail thật và in log cảnh báo ra console
jest.mock('../src/common/services/mail.service', () => {
return {
MailService: jest.fn().mockImplementation(() => {
return {
sendVerificationEmail: jest.fn().mockResolvedValue(undefined),
sendPasswordResetEmail: jest.fn().mockResolvedValue(undefined),
sendNewDeviceAlertEmail: jest.fn().mockResolvedValue(undefined),
sendNotificationEmail: jest.fn().mockResolvedValue(undefined),
};
}),
};
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment