# MCP Module Exploration - GoodGo Platform

## 1. MODULE STRUCTURE & SOURCE FILES

### Directory Structure
```
apps/api/src/modules/mcp/
├── index.ts
├── mcp.module.ts
└── presentation/
    ├── mcp-transport.controller.ts
    └── __tests__/
        └── mcp-transport.controller.spec.ts
```

### All Source Files (4 files)

#### 1. **apps/api/src/modules/mcp/index.ts**
- **Type**: Module entry point (exports)
- **Purpose**: Exports the McpIntegrationModule
- **Exports**: `{ McpIntegrationModule }`

#### 2. **apps/api/src/modules/mcp/mcp.module.ts**
- **Type**: NestJS Module configuration (22 lines)
- **Key Class**: `McpIntegrationModule implements OnModuleInit`
- **Responsibility**: 
  - Sets up MCP core module with configuration
  - Initializes TypesenseClient for MCP registry
  - Logs initialized server names on module init
- **Dependencies Injected**:
  - `TypesenseClientService` (from SearchModule)
  - `McpRegistryService` (from @goodgo/mcp-servers)
  - `LoggerService` (from SharedModule)
- **Imports**:
  - `SearchModule`
  - `AuthModule`
  - `McpCoreModule.forRoot()` with config
- **Controllers**: McpTransportController
- **Lifecycle**: Implements `onModuleInit()`

#### 3. **apps/api/src/modules/mcp/presentation/mcp-transport.controller.ts**
- **Type**: NestJS Controller (102 lines)
- **Key Class**: `McpTransportController`
- **Responsibility**: HTTP transport layer for MCP SSE connections
- **Decorators Applied**:
  - `@ApiTags('mcp')`
  - `@ApiBearerAuth('JWT')`
  - `@Controller('mcp')`
  - `@UseGuards(JwtAuthGuard)` - protects all endpoints
- **Properties**:
  - `transports: Map<string, SSEServerTransport>` - active session management
- **Injected Dependencies**:
  - `registry: McpRegistryService`

**Endpoints:**

1. **GET /mcp/servers** (line 27-34)
   - Summary: List available MCP servers
   - Throttle: 30 requests per 60s
   - Returns: `{ servers: string[] }`
   - Status: 200 (success), 401 (unauthorized)

2. **GET /mcp/:serverName/sse** (line 36-68)
   - Summary: Open SSE connection to MCP server
   - Throttle: 5 requests per 60s (stricter)
   - Params: `serverName`
   - Returns: SSE stream
   - Response Handling:
     - Creates `SSEServerTransport` instance
     - Stores in map with `sessionId`
     - Connects to server via `server.connect(transport)`
     - Cleans up on request close
   - Status: 200 (stream), 404 (server not found), 401 (unauthorized)

3. **POST /mcp/:serverName/messages** (line 70-102)
   - Summary: Send message to MCP server session
   - Throttle: 30 requests per 60s
   - Params: `serverName`
   - Query: `sessionId` (required)
   - Body: Message data passed to transport
   - Response Handling:
     - Validates sessionId exists
     - Delegates to `transport.handlePostMessage(req, res)`
   - Status: 200 (success), 400 (missing sessionId), 404 (session not found), 401 (unauthorized)

---

## 2. TEST FILES

### Total Test Files: 1

#### **apps/api/src/modules/mcp/presentation/__tests__/mcp-transport.controller.spec.ts** (174 lines)
- **Testing Framework**: Vitest
- **Subject**: `McpTransportController`
- **Test Structure**: Describe blocks + beforeEach setup

**Test Suites (4 describe blocks):**

1. **security decorators** (4 tests)
   - Verifies JwtAuthGuard is applied
   - Checks Throttle metadata on endpoints
   - Validates throttle limits (30, 5, 30)

2. **listServers** (2 tests)
   - Returns server list from registry
   - Handles empty server list

3. **handleSse** (3 tests)
   - Throws NOT_FOUND when server doesn't exist
   - Creates transport and connects to server
   - Cleans up on connection close (transport removal)

4. **handleMessage** (2 tests)
   - Throws BAD_REQUEST when sessionId missing
   - Throws NOT_FOUND when session doesn't exist

**Mocking Patterns Used:**
- `vi.mock()` for external modules (SSEServerTransport)
- `vi.fn()` for service method mocks
- `mockReturnValue()`, `mockResolvedValue()`, `mockRejectValue()`
- Manual mock objects for requests/responses
- Reflection API for metadata checks (`Reflect.getMetadata()`)

---

## 3. DDD LAYER STRUCTURE

**Current Status**: MCP module has a **SIMPLIFIED structure** (NOT full DDD yet):

### What EXISTS:
- **Presentation Layer** ✅
  - `presentation/mcp-transport.controller.ts`
  - `presentation/__tests__/mcp-transport.controller.spec.ts`
  - HTTP endpoint definitions
  - Guard decorators (auth)
  - Throttle decorators
  - Swagger decorators

### What DOES NOT EXIST (yet):
- **Domain Layer** ❌
  - No `domain/` directory
  - No entities, value objects, or domain events
  - No domain business logic

- **Application Layer** ❌
  - No `application/` directory
  - No commands/handlers (CQRS pattern not used here)
  - No queries
  - No DTOs

- **Infrastructure Layer** ❌
  - No `infrastructure/` directory
  - No repositories
  - No external service adapters
  - No database access

### Architecture Notes:
- The MCP module acts as an **integration wrapper**
- Delegates to `@goodgo/mcp-servers` library (external dependency)
- Simple **presentation-only controller** approach
- Focuses on HTTP transport mechanism (SSE connections)
- Session management via in-memory Map

---

## 4. KEY CLASSES/HANDLERS NEEDING TESTS

### Current Implementation:

1. **McpIntegrationModule** (mcp.module.ts)
   - **Status**: Partially tested (initialization logic)
   - **Methods needing tests**:
     - `constructor()` - dependency injection
     - `onModuleInit()` - initialization flow
   - **Test focus**: Module setup, service integration, logging

2. **McpTransportController** (mcp-transport.controller.ts)
   - **Status**: Well tested ✅ (174 lines of tests)
   - **Methods tested**:
     - `listServers()` - returns available servers
     - `handleSse()` - establishes SSE connection
     - `handleMessage()` - routes messages to session
   - **Test coverage**: Happy path + error cases + security decorators

---

## 5. TESTING PATTERNS FROM OTHER MODULES

### Pattern 1: Auth Module - Handler Testing (Simple)
**File**: `apps/api/src/modules/auth/application/__tests__/login-user.handler.spec.ts`

```typescript
// KEY PATTERNS:
// 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',
    });
  });
});
```

**Key Takeaways**:
- ✅ Minimal setup, direct handler instantiation
- ✅ Command objects for test input
- ✅ Simple mock with typed object
- ✅ Focus on behavior verification
- ✅ Uses `as any` for type casting


### Pattern 2: Payments Module - Complex Handler Testing (Advanced)
**File**: `apps/api/src/modules/payments/application/__tests__/create-payment.handler.spec.ts`

```typescript
// KEY PATTERNS:
// 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/);
  });
});
```

**Key Takeaways**:
- ✅ Complex multi-dependency mocking
- ✅ Mapped type for repository mocks `[K in keyof IPaymentRepository]`
- ✅ Individual method mocks for each dependency
- ✅ Tests for business rules (idempotency)
- ✅ Error case testing with regex matching
- ✅ Logger mock included
- ✅ Verify multiple calls and interactions


### Pattern 3: Domain Entity Testing (DDD Style)
**File**: `apps/api/src/modules/payments/domain/__tests__/payment.entity.spec.ts`

```typescript
// KEY PATTERNS:
// 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');
  });
});
```

**Key Takeaways**:
- ✅ Explicit vitest imports (explicit over implicit)
- ✅ Helper factory method for complex setup
- ✅ Tests for entity behavior (state transitions)
- ✅ Tests for domain events
- ✅ Tests for error handling (Result pattern)
- ✅ No mocking - tests real entity logic
- ✅ Value object usage (Money.create().unwrap())
- ✅ Domain event verification


### Pattern 4: Infrastructure Service Testing (Crypto/External)
**File**: `apps/api/src/modules/payments/infrastructure/__tests__/zalopay.service.spec.ts`

```typescript
// KEY PATTERNS:
// 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);
  });
});
```

**Key Takeaways**:
- ✅ Explicit vitest imports (import from 'vitest')
- ✅ Tests for security-critical functionality (HMAC verification)
- ✅ ConfigService mocking for environment variables
- ✅ Helper function for complex test data generation
- ✅ Tests for error conditions (invalid JSON, wrong MAC)
- ✅ Tests for security edge cases (tampered data)
- ✅ Uses Node.js crypto module directly
- ✅ Real algorithm testing (SHA256 HMAC)

---

## 6. TESTING CONVENTIONS SUMMARY

### Framework & Setup
- **Framework**: Vitest
- **Environment**: Node.js
- **Globals**: Enabled (`globals: true` in config)
- **File Pattern**: `*.spec.ts` (not `.test.ts`)
- **Test Organization**: `__tests__/` subdirectory

### Import Styles
```typescript
// Style 1: Using globals (in simple tests)
describe('MyClass', () => {
  it('does something', () => {
    expect(true).toBe(true);
  });
});

// Style 2: Explicit imports (in complex/domain tests)
import { describe, it, expect, beforeEach, vi } from 'vitest';
```

### Mocking Patterns
1. **Service Mocks**
   ```typescript
   mockService = {
     method: vi.fn().mockResolvedValue(value),
     method2: vi.fn().mockReturnValue(value),
   };
   ```

2. **Module Mocks**
   ```typescript
   vi.mock('@goodgo/mcp-servers', () => ({
     SSEServerTransport: class MockSSE { /* ... */ },
   }));
   ```

3. **Configuration Mocks**
   ```typescript
   const mockConfig = {
     get: vi.fn((key) => env[key]),
     getOrThrow: vi.fn((key) => env[key] || throw),
   };
   ```

### Test Structure
```typescript
describe('ClassName', () => {
  let instance: ClassName;
  let mockDep1: Mock;
  let mockDep2: Mock;

  beforeEach(() => {
    // Setup mocks
    mockDep1 = { method: vi.fn() };
    // Instantiate with mocks
    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');
    });
  });
});
```

### Common Assertions Used
- `expect(x).toBe(value)` - strict equality
- `expect(x).toEqual(object)` - deep equality
- `expect(x).toHaveLength(n)` - array/string length
- `expect(x).toBeInstanceOf(Class)` - instance check
- `expect(fn).toHaveBeenCalled()` - called verification
- `expect(fn).toHaveBeenCalledWith(args)` - call arguments
- `expect(fn).toHaveBeenCalledTimes(n)` - call count
- `expect(promise).rejects.toThrow(msg)` - error expectation

### Vitest-Specific Features Used
- `vi.fn()` - create mock function
- `vi.mock()` - module mocking
- `mockResolvedValue()` - async return
- `mockReturnValue()` - sync return
- `mockRejectedValue()` - error simulation
- `Reflect.getMetadata()` - decorator inspection (NestJS)

---

## 7. RECOMMENDATIONS FOR MCP MODULE TESTING

### Current Coverage Status
- ✅ Controller (presentation layer): Well tested
- ⚠️ Module initialization: Basic (mocked)
- ❌ Domain layer: Not implemented
- ❌ Application layer: Not implemented
- ❌ Infrastructure layer: Not implemented

### Recommended Next Steps
1. **Expand Controller Tests**
   - Integration tests with mock SSE connections
   - Test session lifecycle (open → message → close)
   - Test error recovery

2. **Add Module Tests**
   - Test `McpIntegrationModule.onModuleInit()`
   - Test registry initialization
   - Test service injection

3. **Consider Adding Domain Tests** (if business logic added)
   - Session entity tests
   - Connection state management
   - Event handling

### Test Command
```bash
# Run all MCP tests
pnpm test -- src/modules/mcp

# Run with coverage
pnpm test -- --coverage src/modules/mcp

# Watch mode
pnpm test -- --watch src/modules/mcp
```
