22 KiB
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:
SearchModuleAuthModuleMcpCoreModule.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:
-
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)
-
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
- Tạo instance
- Trạng thái: 200 (luồng), 404 (không tìm thấy server), 401 (không được phép)
-
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):
-
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)
-
listServers (2 test)
- Trả về danh sách server từ registry
- Xử lý danh sách server rỗng
-
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)
-
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 servicemockReturnValue(),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.tspresentation/__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
- Không có thư mục
-
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
- Không có thư mục
-
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
- Không có thư mục
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:
-
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ộconModuleInit()- 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
-
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ụnghandleSse()- thiết lập kết nối SSEhandleMessage()- đị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
// 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
// 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
// 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
// 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: truetrong 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
// 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
-
Mock Service
mockService = { method: vi.fn().mockResolvedValue(value), method2: vi.fn().mockReturnValue(value), }; -
Mock Module
vi.mock('@goodgo/mcp-servers', () => ({ SSEServerTransport: class MockSSE { /* ... */ }, })); -
Mock Cấu hình
const mockConfig = { get: vi.fn((key) => env[key]), getOrThrow: vi.fn((key) => env[key] || throw), };
Cấu trúc Test
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ặtexpect(x).toEqual(object)- so sánh bằng sâuexpect(x).toHaveLength(n)- độ dài mảng/chuỗiexpect(x).toBeInstanceOf(Class)- kiểm tra instanceexpect(fn).toHaveBeenCalled()- xác minh đã được gọiexpect(fn).toHaveBeenCalledWith(args)- tham số khi gọiexpect(fn).toHaveBeenCalledTimes(n)- số lần gọiexpect(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 mockvi.mock()- mock modulemockResolvedValue()- giá trị trả về bất đồng bộmockReturnValue()- giá trị trả về đồng bộmockRejectedValue()- mô phỏng lỗiReflect.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ị
-
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
-
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
- Kiểm thử
-
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ử
# 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