Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 29s
CI / E2E Tests (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 2m42s
Deploy / Build Web Image (push) Failing after 27s
Deploy / Build AI Services Image (push) Failing after 29s
E2E Tests / Playwright E2E (push) Failing after 43s
Deploy / Build API Image (push) Failing after 1m31s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 6s
Security Scanning / Trivy Scan — API Image (push) Failing after 5m35s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 3m45s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
Security Scanning / Trivy Scan — Web Image (push) Failing after 13m51s
Security Scanning / Trivy Filesystem Scan (push) Failing after 14m46s
Security Scanning / Security Gate (push) Has been cancelled
674 lines
22 KiB
Markdown
674 lines
22 KiB
Markdown
# MCP Module Exploration - GoodGo Platform
|
|
|
|
## 1. CẤU TRÚC MODULE & CÁC TỆP NGUỒN
|
|
|
|
### Cấu trúc thư mục
|
|
```
|
|
apps/api/src/modules/mcp/
|
|
├── index.ts
|
|
├── mcp.module.ts
|
|
└── presentation/
|
|
├── mcp-transport.controller.ts
|
|
└── __tests__/
|
|
└── mcp-transport.controller.spec.ts
|
|
```
|
|
|
|
### Tất cả tệp nguồn (4 tệp)
|
|
|
|
#### 1. **apps/api/src/modules/mcp/index.ts**
|
|
- **Loại**: Điểm vào của module (xuất khẩu)
|
|
- **Mục đích**: Xuất khẩu McpIntegrationModule
|
|
- **Xuất khẩu**: `{ McpIntegrationModule }`
|
|
|
|
#### 2. **apps/api/src/modules/mcp/mcp.module.ts**
|
|
- **Loại**: Cấu hình NestJS Module (22 dòng)
|
|
- **Lớp chính**: `McpIntegrationModule implements OnModuleInit`
|
|
- **Trách nhiệm**:
|
|
- Thiết lập module MCP cốt lõi với cấu hình
|
|
- Khởi tạo TypesenseClient cho registry MCP
|
|
- Ghi log tên các server đã khởi tạo khi module được khởi động
|
|
- **Phụ thuộc được tiêm**:
|
|
- `TypesenseClientService` (từ SearchModule)
|
|
- `McpRegistryService` (từ @goodgo/mcp-servers)
|
|
- `LoggerService` (từ SharedModule)
|
|
- **Nhập khẩu**:
|
|
- `SearchModule`
|
|
- `AuthModule`
|
|
- `McpCoreModule.forRoot()` với cấu hình
|
|
- **Controllers**: McpTransportController
|
|
- **Vòng đời**: Triển khai `onModuleInit()`
|
|
|
|
#### 3. **apps/api/src/modules/mcp/presentation/mcp-transport.controller.ts**
|
|
- **Loại**: NestJS Controller (102 dòng)
|
|
- **Lớp chính**: `McpTransportController`
|
|
- **Trách nhiệm**: Lớp truyền tải HTTP cho các kết nối MCP SSE
|
|
- **Decorator được áp dụng**:
|
|
- `@ApiTags('mcp')`
|
|
- `@ApiBearerAuth('JWT')`
|
|
- `@Controller('mcp')`
|
|
- `@UseGuards(JwtAuthGuard)` - bảo vệ tất cả các endpoint
|
|
- **Thuộc tính**:
|
|
- `transports: Map<string, SSEServerTransport>` - quản lý phiên hoạt động
|
|
- **Phụ thuộc được tiêm**:
|
|
- `registry: McpRegistryService`
|
|
|
|
**Các endpoint:**
|
|
|
|
1. **GET /mcp/servers** (dòng 27-34)
|
|
- Tóm tắt: Liệt kê các MCP server khả dụng
|
|
- Throttle: 30 yêu cầu mỗi 60 giây
|
|
- Trả về: `{ servers: string[] }`
|
|
- Trạng thái: 200 (thành công), 401 (không được phép)
|
|
|
|
2. **GET /mcp/:serverName/sse** (dòng 36-68)
|
|
- Tóm tắt: Mở kết nối SSE đến MCP server
|
|
- Throttle: 5 yêu cầu mỗi 60 giây (chặt chẽ hơn)
|
|
- Tham số: `serverName`
|
|
- Trả về: Luồng SSE
|
|
- Xử lý phản hồi:
|
|
- Tạo instance `SSEServerTransport`
|
|
- Lưu vào map với `sessionId`
|
|
- Kết nối đến server qua `server.connect(transport)`
|
|
- Dọn dẹp khi yêu cầu đóng
|
|
- Trạng thái: 200 (luồng), 404 (không tìm thấy server), 401 (không được phép)
|
|
|
|
3. **POST /mcp/:serverName/messages** (dòng 70-102)
|
|
- Tóm tắt: Gửi tin nhắn đến phiên MCP server
|
|
- Throttle: 30 yêu cầu mỗi 60 giây
|
|
- Tham số: `serverName`
|
|
- Query: `sessionId` (bắt buộc)
|
|
- Body: Dữ liệu tin nhắn được truyền đến transport
|
|
- Xử lý phản hồi:
|
|
- Xác thực sessionId tồn tại
|
|
- Ủy quyền cho `transport.handlePostMessage(req, res)`
|
|
- Trạng thái: 200 (thành công), 400 (thiếu sessionId), 404 (không tìm thấy phiên), 401 (không được phép)
|
|
|
|
---
|
|
|
|
## 2. CÁC TỆP TEST
|
|
|
|
### Tổng số tệp test: 1
|
|
|
|
#### **apps/api/src/modules/mcp/presentation/__tests__/mcp-transport.controller.spec.ts** (174 dòng)
|
|
- **Framework kiểm thử**: Vitest
|
|
- **Đối tượng kiểm thử**: `McpTransportController`
|
|
- **Cấu trúc test**: Các khối Describe + thiết lập beforeEach
|
|
|
|
**Bộ test (4 khối describe):**
|
|
|
|
1. **security decorators** (4 test)
|
|
- Xác minh JwtAuthGuard được áp dụng
|
|
- Kiểm tra metadata Throttle trên các endpoint
|
|
- Xác nhận giới hạn throttle (30, 5, 30)
|
|
|
|
2. **listServers** (2 test)
|
|
- Trả về danh sách server từ registry
|
|
- Xử lý danh sách server rỗng
|
|
|
|
3. **handleSse** (3 test)
|
|
- Ném NOT_FOUND khi server không tồn tại
|
|
- Tạo transport và kết nối đến server
|
|
- Dọn dẹp khi đóng kết nối (xóa transport)
|
|
|
|
4. **handleMessage** (2 test)
|
|
- Ném BAD_REQUEST khi thiếu sessionId
|
|
- Ném NOT_FOUND khi phiên không tồn tại
|
|
|
|
**Các mẫu Mock được sử dụng:**
|
|
- `vi.mock()` cho các module ngoài (SSEServerTransport)
|
|
- `vi.fn()` cho mock phương thức service
|
|
- `mockReturnValue()`, `mockResolvedValue()`, `mockRejectValue()`
|
|
- Đối tượng mock thủ công cho request/response
|
|
- Reflection API để kiểm tra metadata (`Reflect.getMetadata()`)
|
|
|
|
---
|
|
|
|
## 3. CẤU TRÚC LỚP DDD
|
|
|
|
**Trạng thái hiện tại**: Module MCP có **cấu trúc ĐƠN GIẢN HÓA** (chưa áp dụng DDD đầy đủ):
|
|
|
|
### Những gì ĐÃ TỒN TẠI:
|
|
- **Lớp Presentation** ✅
|
|
- `presentation/mcp-transport.controller.ts`
|
|
- `presentation/__tests__/mcp-transport.controller.spec.ts`
|
|
- Định nghĩa HTTP endpoint
|
|
- Decorator Guard (xác thực)
|
|
- Decorator Throttle
|
|
- Decorator Swagger
|
|
|
|
### Những gì CHƯA TỒN TẠI:
|
|
- **Lớp Domain** ❌
|
|
- Không có thư mục `domain/`
|
|
- Không có entity, value object hay domain event
|
|
- Không có logic nghiệp vụ domain
|
|
|
|
- **Lớp Application** ❌
|
|
- Không có thư mục `application/`
|
|
- Không có command/handler (chưa dùng mẫu CQRS)
|
|
- Không có query
|
|
- Không có DTO
|
|
|
|
- **Lớp Infrastructure** ❌
|
|
- Không có thư mục `infrastructure/`
|
|
- Không có repository
|
|
- Không có adapter dịch vụ ngoài
|
|
- Không có truy cập cơ sở dữ liệu
|
|
|
|
### Ghi chú kiến trúc:
|
|
- Module MCP đóng vai trò là một **lớp tích hợp bọc ngoài**
|
|
- Ủy quyền cho thư viện `@goodgo/mcp-servers` (phụ thuộc ngoài)
|
|
- Cách tiếp cận **controller chỉ có presentation** đơn giản
|
|
- Tập trung vào cơ chế truyền tải HTTP (kết nối SSE)
|
|
- Quản lý phiên qua Map trong bộ nhớ
|
|
|
|
---
|
|
|
|
## 4. CÁC LỚP/HANDLER CHÍNH CẦN ĐƯỢC KIỂM THỬ
|
|
|
|
### Triển khai hiện tại:
|
|
|
|
1. **McpIntegrationModule** (mcp.module.ts)
|
|
- **Trạng thái**: Được kiểm thử một phần (logic khởi tạo)
|
|
- **Các phương thức cần kiểm thử**:
|
|
- `constructor()` - tiêm phụ thuộc
|
|
- `onModuleInit()` - luồng khởi tạo
|
|
- **Trọng tâm kiểm thử**: Thiết lập module, tích hợp service, ghi log
|
|
|
|
2. **McpTransportController** (mcp-transport.controller.ts)
|
|
- **Trạng thái**: Được kiểm thử tốt ✅ (174 dòng test)
|
|
- **Các phương thức được kiểm thử**:
|
|
- `listServers()` - trả về các server khả dụng
|
|
- `handleSse()` - thiết lập kết nối SSE
|
|
- `handleMessage()` - định tuyến tin nhắn đến phiên
|
|
- **Độ phủ kiểm thử**: Happy path + trường hợp lỗi + decorator bảo mật
|
|
|
|
---
|
|
|
|
## 5. CÁC MẪU KIỂM THỬ TỪ CÁC MODULE KHÁC
|
|
|
|
### Mẫu 1: Module Auth - Kiểm thử Handler (Đơn giản)
|
|
**Tệp**: `apps/api/src/modules/auth/application/__tests__/login-user.handler.spec.ts`
|
|
|
|
```typescript
|
|
// CÁC MẪU CHÍNH:
|
|
// 1. No explicit imports from vitest (globals enabled)
|
|
// 2. beforeEach: Create handler with mocked dependencies
|
|
// 3. vi.fn() for service mocks
|
|
// 4. mockResolvedValue() for async returns
|
|
// 5. expect().toHaveBeenCalledWith() for verification
|
|
// 6. Simple test structure for handlers
|
|
|
|
describe('LoginUserHandler', () => {
|
|
let handler: LoginUserHandler;
|
|
let mockTokenService: { generateTokenPair: ReturnType<typeof vi.fn> };
|
|
|
|
const tokenPair = {
|
|
accessToken: 'access-jwt',
|
|
refreshToken: 'family.refresh-hex',
|
|
expiresIn: 900,
|
|
};
|
|
|
|
beforeEach(() => {
|
|
mockTokenService = { generateTokenPair: vi.fn().mockResolvedValue(tokenPair) };
|
|
handler = new LoginUserHandler(mockTokenService as any);
|
|
});
|
|
|
|
it('generates token pair with correct payload', async () => {
|
|
const command = new LoginUserCommand('user-1', '0912345678', 'BUYER');
|
|
const result = await handler.execute(command);
|
|
|
|
expect(result).toEqual(tokenPair);
|
|
expect(mockTokenService.generateTokenPair).toHaveBeenCalledWith({
|
|
sub: 'user-1',
|
|
phone: '0912345678',
|
|
role: 'BUYER',
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
**Điểm chú ý chính**:
|
|
- ✅ Thiết lập tối giản, khởi tạo handler trực tiếp
|
|
- ✅ Đối tượng Command làm đầu vào kiểm thử
|
|
- ✅ Mock đơn giản với đối tượng có kiểu
|
|
- ✅ Tập trung vào xác minh hành vi
|
|
- ✅ Dùng `as any` để ép kiểu
|
|
|
|
|
|
### Mẫu 2: Module Payments - Kiểm thử Handler phức tạp (Nâng cao)
|
|
**Tệp**: `apps/api/src/modules/payments/application/__tests__/create-payment.handler.spec.ts`
|
|
|
|
```typescript
|
|
// CÁC MẪU CHÍNH:
|
|
// 1. Multiple mocked dependencies (repo, factory, gateway, event bus)
|
|
// 2. Complex mock setup with multiple methods
|
|
// 3. Tests for idempotency handling
|
|
// 4. Error case validation
|
|
// 5. Event publishing verification
|
|
|
|
describe('CreatePaymentHandler', () => {
|
|
let handler: CreatePaymentHandler;
|
|
let mockPaymentRepo: { [K in keyof IPaymentRepository]: ReturnType<typeof vi.fn> };
|
|
let mockGatewayFactory: { getGateway: ReturnType<typeof vi.fn> };
|
|
let mockGateway: {
|
|
createPaymentUrl: ReturnType<typeof vi.fn>;
|
|
verifyCallback: ReturnType<typeof vi.fn>;
|
|
refund: ReturnType<typeof vi.fn>;
|
|
};
|
|
let mockEventBus: { publish: ReturnType<typeof vi.fn> };
|
|
|
|
beforeEach(() => {
|
|
mockPaymentRepo = {
|
|
findById: vi.fn(),
|
|
findByProviderTxId: vi.fn(),
|
|
findByIdempotencyKey: vi.fn(),
|
|
findByUserId: vi.fn(),
|
|
save: vi.fn().mockResolvedValue(undefined),
|
|
update: vi.fn(),
|
|
updateIfStatus: vi.fn(),
|
|
};
|
|
|
|
mockGateway = {
|
|
createPaymentUrl: vi.fn().mockResolvedValue({
|
|
paymentUrl: 'https://vnpay.vn/pay/123',
|
|
providerTxId: 'vnpay-tx-1',
|
|
}),
|
|
verifyCallback: vi.fn(),
|
|
refund: vi.fn(),
|
|
};
|
|
|
|
mockGatewayFactory = {
|
|
getGateway: vi.fn().mockReturnValue(mockGateway),
|
|
};
|
|
|
|
mockEventBus = { publish: vi.fn() };
|
|
const mockLogger = { log: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), verbose: vi.fn() };
|
|
|
|
handler = new CreatePaymentHandler(
|
|
mockPaymentRepo as any,
|
|
mockGatewayFactory as any,
|
|
mockEventBus as any,
|
|
mockLogger as any,
|
|
);
|
|
});
|
|
|
|
it('creates payment successfully', async () => {
|
|
mockPaymentRepo.findByIdempotencyKey.mockResolvedValue(null);
|
|
|
|
const command = new CreatePaymentCommand(
|
|
'user-1', 'VNPAY', 'SUBSCRIPTION', 500_000n,
|
|
'Thanh toán gói Pro', 'https://goodgo.vn/return', '127.0.0.1',
|
|
undefined, 'idem-key-1',
|
|
);
|
|
const result = await handler.execute(command);
|
|
|
|
expect(result.paymentId).toBeDefined();
|
|
expect(result.paymentUrl).toBe('https://vnpay.vn/pay/123');
|
|
expect(result.providerTxId).toBe('vnpay-tx-1');
|
|
expect(mockPaymentRepo.save).toHaveBeenCalledTimes(1);
|
|
expect(mockEventBus.publish).toHaveBeenCalled();
|
|
expect(mockGatewayFactory.getGateway).toHaveBeenCalledWith('VNPAY');
|
|
});
|
|
|
|
it('throws ConflictException for duplicate idempotency key (pending)', async () => {
|
|
mockPaymentRepo.findByIdempotencyKey.mockResolvedValue({ status: 'PENDING' });
|
|
|
|
const command = new CreatePaymentCommand(
|
|
'user-1', 'VNPAY', 'SUBSCRIPTION', 500_000n,
|
|
'desc', 'https://goodgo.vn/return', '127.0.0.1',
|
|
undefined, 'existing-key',
|
|
);
|
|
|
|
await expect(handler.execute(command)).rejects.toThrow(/idempotency/);
|
|
});
|
|
});
|
|
```
|
|
|
|
**Điểm chú ý chính**:
|
|
- ✅ Mock nhiều phụ thuộc phức tạp
|
|
- ✅ Kiểu ánh xạ cho mock repository `[K in keyof IPaymentRepository]`
|
|
- ✅ Mock từng phương thức riêng lẻ cho mỗi phụ thuộc
|
|
- ✅ Kiểm thử quy tắc nghiệp vụ (idempotency)
|
|
- ✅ Kiểm thử trường hợp lỗi với khớp regex
|
|
- ✅ Bao gồm mock logger
|
|
- ✅ Xác minh nhiều lần gọi và tương tác
|
|
|
|
|
|
### Mẫu 3: Kiểm thử Entity Domain (Phong cách DDD)
|
|
**Tệp**: `apps/api/src/modules/payments/domain/__tests__/payment.entity.spec.ts`
|
|
|
|
```typescript
|
|
// CÁC MẪU CHÍNH:
|
|
// 1. Explicit vitest imports at the top
|
|
// 2. Complex entity with state transitions
|
|
// 3. Tests for domain events
|
|
// 4. Tests for value objects
|
|
// 5. Tests for error handling (Result types)
|
|
// 6. Helper factory function for test setup
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import { PaymentEntity } from '../entities/payment.entity';
|
|
import { PaymentCompletedEvent } from '../events/payment-completed.event';
|
|
import { PaymentCreatedEvent } from '../events/payment-created.event';
|
|
import { PaymentFailedEvent } from '../events/payment-failed.event';
|
|
import { PaymentRefundedEvent } from '../events/payment-refunded.event';
|
|
import { Money } from '../value-objects/money.vo';
|
|
|
|
describe('PaymentEntity', () => {
|
|
const createPayment = (status?: string) => {
|
|
const money = Money.create(500_000n).unwrap();
|
|
const payment = PaymentEntity.createNew(
|
|
'pay-1',
|
|
'user-1',
|
|
'VNPAY',
|
|
'LISTING_FEE',
|
|
money,
|
|
'txn-1',
|
|
'idem-key-1',
|
|
);
|
|
if (status === 'PROCESSING') {
|
|
payment.markProcessing('vnp-txn-123');
|
|
}
|
|
if (status === 'COMPLETED') {
|
|
payment.markProcessing('vnp-txn-123');
|
|
payment.clearDomainEvents();
|
|
payment.markCompleted({ responseCode: '00' });
|
|
}
|
|
return payment;
|
|
};
|
|
|
|
it('should create a new payment with domain events', () => {
|
|
const money = Money.create(500_000n).unwrap();
|
|
const payment = PaymentEntity.createNew(
|
|
'pay-1',
|
|
'user-1',
|
|
'VNPAY',
|
|
'LISTING_FEE',
|
|
money,
|
|
'txn-1',
|
|
);
|
|
|
|
expect(payment.id).toBe('pay-1');
|
|
expect(payment.status).toBe('PENDING');
|
|
|
|
const events = payment.domainEvents;
|
|
expect(events).toHaveLength(1);
|
|
expect(events[0]).toBeInstanceOf(PaymentCreatedEvent);
|
|
});
|
|
|
|
it('should mark completed payment as refunded and emit event', () => {
|
|
const payment = createPayment('COMPLETED');
|
|
payment.clearDomainEvents();
|
|
const result = payment.markRefunded();
|
|
expect(result.isOk).toBe(true);
|
|
expect(payment.status).toBe('REFUNDED');
|
|
|
|
const events = payment.domainEvents;
|
|
expect(events).toHaveLength(1);
|
|
expect(events[0]).toBeInstanceOf(PaymentRefundedEvent);
|
|
});
|
|
|
|
it('should not refund a non-completed payment', () => {
|
|
const payment = createPayment();
|
|
const result = payment.markRefunded();
|
|
expect(result.isErr).toBe(true);
|
|
expect(result.unwrapErr().message).toContain('hoàn tiền');
|
|
});
|
|
});
|
|
```
|
|
|
|
**Điểm chú ý chính**:
|
|
- ✅ Import vitest tường minh (tường minh hơn ngầm định)
|
|
- ✅ Phương thức factory trợ giúp cho thiết lập phức tạp
|
|
- ✅ Kiểm thử hành vi entity (chuyển đổi trạng thái)
|
|
- ✅ Kiểm thử domain event
|
|
- ✅ Kiểm thử xử lý lỗi (mẫu Result)
|
|
- ✅ Không mock - kiểm thử logic entity thực tế
|
|
- ✅ Sử dụng value object (Money.create().unwrap())
|
|
- ✅ Xác minh domain event
|
|
|
|
|
|
### Mẫu 4: Kiểm thử Service Infrastructure (Crypto/Bên ngoài)
|
|
**Tệp**: `apps/api/src/modules/payments/infrastructure/__tests__/zalopay.service.spec.ts`
|
|
|
|
```typescript
|
|
// CÁC MẪU CHÍNH:
|
|
// 1. Explicit vitest imports
|
|
// 2. External service simulation (payment gateway)
|
|
// 3. Crypto/HMAC signature testing
|
|
// 4. Helper function to build test data
|
|
// 5. Mocking ConfigService
|
|
// 6. Tests for security (tamper detection)
|
|
|
|
import * as crypto from 'crypto';
|
|
import { type ConfigService } from '@nestjs/config';
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { ZalopayService } from '../services/zalopay.service';
|
|
|
|
describe('ZalopayService', () => {
|
|
let service: ZalopayService;
|
|
const key2 = 'TESTKEY2ABCDEF1234567890ABCDEF12';
|
|
|
|
beforeEach(() => {
|
|
const mockConfig = {
|
|
get: vi.fn((key: string, defaultValue?: string) => {
|
|
const env: Record<string, string> = {
|
|
'ZALOPAY_APP_ID': '2553',
|
|
'ZALOPAY_KEY1': 'TESTKEY1ABCDEF1234567890ABCDEF12',
|
|
'ZALOPAY_KEY2': 'TESTKEY2ABCDEF1234567890ABCDEF12',
|
|
};
|
|
return env[key] ?? defaultValue;
|
|
}),
|
|
getOrThrow: vi.fn((key: string) => {
|
|
// ...
|
|
}),
|
|
} as unknown as ConfigService;
|
|
const mockLogger = { log: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), verbose: vi.fn() };
|
|
service = new ZalopayService(mockConfig, mockLogger as any);
|
|
});
|
|
|
|
function buildCallbackData(
|
|
dataPayload: Record<string, unknown> = {},
|
|
tamperMac = false,
|
|
): Record<string, string> {
|
|
const payload = {
|
|
app_id: 2553,
|
|
app_trans_id: '260408_order-123',
|
|
zp_trans_id: 'ZLP_TX_456',
|
|
...dataPayload,
|
|
};
|
|
const dataStr = JSON.stringify(payload);
|
|
let mac = crypto
|
|
.createHmac('sha256', key2)
|
|
.update(dataStr)
|
|
.digest('hex');
|
|
|
|
if (tamperMac) {
|
|
mac = 'a'.repeat(mac.length);
|
|
}
|
|
|
|
return { data: dataStr, mac };
|
|
}
|
|
|
|
it('should verify a valid callback with timingSafeEqual', () => {
|
|
const data = buildCallbackData();
|
|
const result = service.verifyCallback(data);
|
|
|
|
expect(result.isValid).toBe(true);
|
|
expect(result.isSuccess).toBe(true);
|
|
expect(result.orderId).toBe('260408_order-123');
|
|
expect(result.providerTxId).toBe('ZLP_TX_456');
|
|
});
|
|
|
|
it('should reject an invalid MAC', () => {
|
|
const data = buildCallbackData({}, true);
|
|
const result = service.verifyCallback(data);
|
|
|
|
expect(result.isValid).toBe(false);
|
|
});
|
|
|
|
it('should handle invalid JSON in data field gracefully', () => {
|
|
const mac = crypto
|
|
.createHmac('sha256', key2)
|
|
.update('not-json')
|
|
.digest('hex');
|
|
|
|
const result = service.verifyCallback({ data: 'not-json', mac });
|
|
expect(result.isValid).toBe(false);
|
|
});
|
|
});
|
|
```
|
|
|
|
**Điểm chú ý chính**:
|
|
- ✅ Import vitest tường minh (import from 'vitest')
|
|
- ✅ Kiểm thử chức năng quan trọng về bảo mật (xác minh HMAC)
|
|
- ✅ Mock ConfigService cho biến môi trường
|
|
- ✅ Hàm trợ giúp để tạo dữ liệu kiểm thử phức tạp
|
|
- ✅ Kiểm thử các điều kiện lỗi (JSON không hợp lệ, MAC sai)
|
|
- ✅ Kiểm thử các trường hợp biên về bảo mật (dữ liệu bị giả mạo)
|
|
- ✅ Sử dụng module crypto của Node.js trực tiếp
|
|
- ✅ Kiểm thử thuật toán thực (SHA256 HMAC)
|
|
|
|
---
|
|
|
|
## 6. TÓM TẮT QUY ƯỚC KIỂM THỬ
|
|
|
|
### Framework & Thiết lập
|
|
- **Framework**: Vitest
|
|
- **Môi trường**: Node.js
|
|
- **Globals**: Được bật (`globals: true` trong cấu hình)
|
|
- **Mẫu tên tệp**: `*.spec.ts` (không phải `.test.ts`)
|
|
- **Tổ chức test**: Thư mục con `__tests__/`
|
|
|
|
### Phong cách Import
|
|
```typescript
|
|
// Phong cách 1: Dùng globals (trong các test đơn giản)
|
|
describe('MyClass', () => {
|
|
it('does something', () => {
|
|
expect(true).toBe(true);
|
|
});
|
|
});
|
|
|
|
// Phong cách 2: Import tường minh (trong các test phức tạp/domain)
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
```
|
|
|
|
### Các mẫu Mock
|
|
1. **Mock Service**
|
|
```typescript
|
|
mockService = {
|
|
method: vi.fn().mockResolvedValue(value),
|
|
method2: vi.fn().mockReturnValue(value),
|
|
};
|
|
```
|
|
|
|
2. **Mock Module**
|
|
```typescript
|
|
vi.mock('@goodgo/mcp-servers', () => ({
|
|
SSEServerTransport: class MockSSE { /* ... */ },
|
|
}));
|
|
```
|
|
|
|
3. **Mock Cấu hình**
|
|
```typescript
|
|
const mockConfig = {
|
|
get: vi.fn((key) => env[key]),
|
|
getOrThrow: vi.fn((key) => env[key] || throw),
|
|
};
|
|
```
|
|
|
|
### Cấu trúc Test
|
|
```typescript
|
|
describe('ClassName', () => {
|
|
let instance: ClassName;
|
|
let mockDep1: Mock;
|
|
let mockDep2: Mock;
|
|
|
|
beforeEach(() => {
|
|
// Thiết lập mock
|
|
mockDep1 = { method: vi.fn() };
|
|
// Khởi tạo với mock
|
|
instance = new ClassName(mockDep1 as any, mockDep2 as any);
|
|
});
|
|
|
|
describe('methodName', () => {
|
|
it('happy path scenario', async () => {
|
|
// Arrange
|
|
mockDep1.method.mockResolvedValue(expected);
|
|
|
|
// Act
|
|
const result = await instance.method();
|
|
|
|
// Assert
|
|
expect(result).toEqual(expected);
|
|
expect(mockDep1.method).toHaveBeenCalledWith(params);
|
|
});
|
|
|
|
it('error case', async () => {
|
|
// Arrange
|
|
mockDep1.method.mockRejectedValue(new Error('fail'));
|
|
|
|
// Act & Assert
|
|
await expect(instance.method()).rejects.toThrow('fail');
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
### Các assertion thường dùng
|
|
- `expect(x).toBe(value)` - so sánh bằng nghiêm ngặt
|
|
- `expect(x).toEqual(object)` - so sánh bằng sâu
|
|
- `expect(x).toHaveLength(n)` - độ dài mảng/chuỗi
|
|
- `expect(x).toBeInstanceOf(Class)` - kiểm tra instance
|
|
- `expect(fn).toHaveBeenCalled()` - xác minh đã được gọi
|
|
- `expect(fn).toHaveBeenCalledWith(args)` - tham số khi gọi
|
|
- `expect(fn).toHaveBeenCalledTimes(n)` - số lần gọi
|
|
- `expect(promise).rejects.toThrow(msg)` - kỳ vọng lỗi
|
|
|
|
### Các tính năng đặc trưng của Vitest được dùng
|
|
- `vi.fn()` - tạo hàm mock
|
|
- `vi.mock()` - mock module
|
|
- `mockResolvedValue()` - giá trị trả về bất đồng bộ
|
|
- `mockReturnValue()` - giá trị trả về đồng bộ
|
|
- `mockRejectedValue()` - mô phỏng lỗi
|
|
- `Reflect.getMetadata()` - kiểm tra decorator (NestJS)
|
|
|
|
---
|
|
|
|
## 7. KHUYẾN NGHỊ CHO KIỂM THỬ MODULE MCP
|
|
|
|
### Trạng thái độ phủ hiện tại
|
|
- ✅ Controller (lớp presentation): Được kiểm thử tốt
|
|
- ⚠️ Khởi tạo module: Cơ bản (được mock)
|
|
- ❌ Lớp Domain: Chưa được triển khai
|
|
- ❌ Lớp Application: Chưa được triển khai
|
|
- ❌ Lớp Infrastructure: Chưa được triển khai
|
|
|
|
### Các bước tiếp theo được khuyến nghị
|
|
1. **Mở rộng kiểm thử Controller**
|
|
- Kiểm thử tích hợp với kết nối SSE được mock
|
|
- Kiểm thử vòng đời phiên (mở → tin nhắn → đóng)
|
|
- Kiểm thử phục hồi lỗi
|
|
|
|
2. **Thêm kiểm thử Module**
|
|
- Kiểm thử `McpIntegrationModule.onModuleInit()`
|
|
- Kiểm thử khởi tạo registry
|
|
- Kiểm thử tiêm service
|
|
|
|
3. **Cân nhắc thêm kiểm thử Domain** (nếu có logic nghiệp vụ)
|
|
- Kiểm thử entity phiên
|
|
- Quản lý trạng thái kết nối
|
|
- Xử lý sự kiện
|
|
|
|
### Lệnh kiểm thử
|
|
```bash
|
|
# Chạy tất cả test MCP
|
|
pnpm test -- src/modules/mcp
|
|
|
|
# Chạy với coverage
|
|
pnpm test -- --coverage src/modules/mcp
|
|
|
|
# Chế độ watch
|
|
pnpm test -- --watch src/modules/mcp
|
|
```
|