# Backup Service Testing Guide

## Overview

The backup-service implements a comprehensive testing strategy with three levels:

1. **Unit Tests** - Test individual components in isolation
2. **Integration Tests** - Test component interactions with real database
3. **E2E Tests** - Test complete API workflows

---

## Test Structure

```
test/
├── unit/                              # Unit tests (37 tests)
│   ├── api/
│   │   ├── backup-share.controller.spec.ts
│   │   └── health.controller.spec.ts
│   ├── application/
│   │   ├── store-backup-share.handler.spec.ts
│   │   ├── get-backup-share.handler.spec.ts
│   │   └── revoke-share.handler.spec.ts
│   ├── domain/
│   │   ├── backup-share.entity.spec.ts
│   │   └── value-objects.spec.ts
│   ├── infrastructure/
│   │   └── aes-encryption.service.spec.ts
│   └── shared/
│       ├── audit-log.interceptor.spec.ts
│       ├── global-exception.filter.spec.ts
│       └── service-auth.guard.spec.ts
│
├── integration/                       # Integration tests
│   ├── backup-share-repository.integration.spec.ts
│   └── audit-log-repository.integration.spec.ts
│
├── e2e/                              # E2E tests (20 tests)
│   ├── backup-share.e2e-spec.ts      # Real database
│   └── backup-share-mock.e2e-spec.ts # Mocked services
│
├── setup/                            # Test infrastructure
│   ├── global-setup.ts
│   ├── global-teardown.ts
│   ├── jest-e2e-setup.ts
│   ├── jest-mock-setup.ts
│   └── test-database.helper.ts
│
└── utils/                            # Test utilities
    ├── mock-prisma.service.ts
    └── test-utils.ts
```

---

## Running Tests

### Quick Commands

```bash
# Run all unit tests
npm run test:unit

# Run E2E tests with mocked services (fast)
npm run test:e2e:mock

# Run E2E tests with real database
npm run test:e2e:db

# Run all tests (unit + mock E2E)
npm run test:all

# Generate coverage report
npm run test:cov

# Watch mode for development
npm run test:watch
```

### Test Configurations

| Config File | Purpose | Command |
|------------|---------|---------|
| `jest.config.js` | Unit tests | `npm run test:unit` |
| `test/jest-e2e-mock.json` | E2E with mocks | `npm run test:e2e:mock` |
| `test/jest-e2e-db.json` | E2E with real DB | `npm run test:e2e:db` |

---

## Unit Testing

### Philosophy

- Test each component in isolation
- Mock all dependencies
- Focus on business logic and edge cases
- Fast execution (< 5 seconds total)

### Example: Testing a Handler

```typescript
// test/unit/application/store-backup-share.handler.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { StoreBackupShareHandler } from '../../../src/application/commands/store-backup-share/store-backup-share.handler';
import { StoreBackupShareCommand } from '../../../src/application/commands/store-backup-share/store-backup-share.command';
import { BackupShareRepository } from '../../../src/domain/repositories/backup-share.repository.interface';
import { AesEncryptionService } from '../../../src/infrastructure/crypto/aes-encryption.service';
import { AuditLogRepository } from '../../../src/infrastructure/persistence/repositories/audit-log.repository';

describe('StoreBackupShareHandler', () => {
  let handler: StoreBackupShareHandler;
  let mockRepository: jest.Mocked<BackupShareRepository>;
  let mockEncryptionService: jest.Mocked<AesEncryptionService>;
  let mockAuditLogRepository: jest.Mocked<AuditLogRepository>;

  beforeEach(async () => {
    mockRepository = {
      save: jest.fn(),
      findByUserId: jest.fn(),
      findByPublicKey: jest.fn(),
      // ... other methods
    };

    mockEncryptionService = {
      encrypt: jest.fn().mockResolvedValue({
        encrypted: 'encrypted-data',
        keyId: 'key-v1',
      }),
    };

    mockAuditLogRepository = {
      log: jest.fn().mockResolvedValue(undefined),
    };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        StoreBackupShareHandler,
        { provide: 'BackupShareRepository', useValue: mockRepository },
        { provide: AesEncryptionService, useValue: mockEncryptionService },
        { provide: AuditLogRepository, useValue: mockAuditLogRepository },
      ],
    }).compile();

    handler = module.get<StoreBackupShareHandler>(StoreBackupShareHandler);
  });

  describe('execute', () => {
    it('should store backup share successfully', async () => {
      // Arrange
      const command = new StoreBackupShareCommand(
        '12345',
        1001,
        '02' + 'a'.repeat(64),
        'encrypted-share-data',
        'identity-service',
        '127.0.0.1'
      );

      mockRepository.findByUserId.mockResolvedValue(null);
      mockRepository.findByPublicKey.mockResolvedValue(null);
      mockRepository.save.mockResolvedValue({
        shareId: BigInt(1),
        // ... other fields
      });

      // Act
      const result = await handler.execute(command);

      // Assert
      expect(result.shareId).toBe('1');
      expect(mockRepository.save).toHaveBeenCalled();
      expect(mockEncryptionService.encrypt).toHaveBeenCalledWith('encrypted-share-data');
      expect(mockAuditLogRepository.log).toHaveBeenCalled();
    });

    it('should throw error if share already exists for user', async () => {
      // Arrange
      const command = new StoreBackupShareCommand(/*...*/);
      mockRepository.findByUserId.mockResolvedValue({ /* existing share */ });

      // Act & Assert
      await expect(handler.execute(command)).rejects.toThrow('SHARE_ALREADY_EXISTS');
    });
  });
});
```

### Example: Testing a Controller

```typescript
// test/unit/api/backup-share.controller.spec.ts
describe('BackupShareController', () => {
  let controller: BackupShareController;
  let mockService: jest.Mocked<BackupShareApplicationService>;

  beforeEach(async () => {
    mockService = {
      storeBackupShare: jest.fn(),
      getBackupShare: jest.fn(),
      revokeShare: jest.fn(),
    };

    const module: TestingModule = await Test.createTestingModule({
      controllers: [BackupShareController],
      providers: [
        { provide: BackupShareApplicationService, useValue: mockService },
      ],
    }).compile();

    controller = module.get<BackupShareController>(BackupShareController);
  });

  describe('storeShare', () => {
    it('should return success response', async () => {
      // Arrange
      const dto: StoreShareDto = {
        userId: '12345',
        accountSequence: 1001,
        publicKey: '02' + 'a'.repeat(64),
        encryptedShareData: 'test-data',
      };

      const mockRequest = {
        sourceService: 'identity-service',
        sourceIp: '127.0.0.1',
      };

      mockService.storeBackupShare.mockResolvedValue({ shareId: '1' });

      // Act
      const result = await controller.storeShare(dto, mockRequest);

      // Assert
      expect(result.success).toBe(true);
      expect(result.shareId).toBe('1');
    });
  });
});
```

### Example: Testing Domain Entity

```typescript
// test/unit/domain/backup-share.entity.spec.ts
describe('BackupShare Entity', () => {
  describe('create', () => {
    it('should create a valid backup share', () => {
      const share = BackupShare.create({
        userId: BigInt(12345),
        accountSequence: BigInt(1001),
        publicKey: '02' + 'a'.repeat(64),
        encryptedShareData: 'encrypted-data',
        encryptionKeyId: 'key-v1',
      });

      expect(share.userId).toBe(BigInt(12345));
      expect(share.status).toBe(BackupShareStatus.ACTIVE);
      expect(share.partyIndex).toBe(2);
      expect(share.accessCount).toBe(0);
    });

    it('should throw error for invalid public key length', () => {
      expect(() => BackupShare.create({
        // ... with short publicKey
        publicKey: 'short',
      })).toThrow();
    });
  });

  describe('revoke', () => {
    it('should mark share as revoked', () => {
      const share = BackupShare.create({/*...*/});

      share.revoke('ROTATION');

      expect(share.status).toBe(BackupShareStatus.REVOKED);
      expect(share.revokedAt).toBeDefined();
    });
  });

  describe('recordAccess', () => {
    it('should increment access count', () => {
      const share = BackupShare.create({/*...*/});

      share.recordAccess();
      share.recordAccess();

      expect(share.accessCount).toBe(2);
      expect(share.lastAccessedAt).toBeDefined();
    });
  });
});
```

---

## E2E Testing

### Mock E2E Tests

Fast tests using mocked Prisma service:

```typescript
// test/e2e/backup-share-mock.e2e-spec.ts
describe('BackupShare E2E (Mocked)', () => {
  let app: INestApplication;
  let mockPrismaService: MockPrismaService;

  beforeAll(async () => {
    const moduleFixture = await Test.createTestingModule({
      imports: [AppModule],
    })
      .overrideProvider(PrismaService)
      .useClass(MockPrismaService)
      .compile();

    app = moduleFixture.createNestApplication();
    app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
    await app.init();

    mockPrismaService = app.get(MockPrismaService);
  });

  it('should store backup share successfully', async () => {
    mockPrismaService.backupShare.findUnique.mockResolvedValue(null);
    mockPrismaService.backupShare.create.mockResolvedValue({
      shareId: BigInt(1),
      // ... mock data
    });

    const response = await request(app.getHttpServer())
      .post('/backup-share/store')
      .set('X-Service-Token', generateServiceToken('identity-service'))
      .send({
        userId: '12345',
        accountSequence: 1001,
        publicKey: '02' + 'a'.repeat(64),
        encryptedShareData: 'test-data',
      })
      .expect(201);

    expect(response.body.success).toBe(true);
  });
});
```

### Real Database E2E Tests

Tests with actual PostgreSQL database:

```typescript
// test/e2e/backup-share.e2e-spec.ts
describe('BackupShare E2E (Real Database)', () => {
  let app: INestApplication;
  let prisma: PrismaService;
  let serviceToken: string;

  beforeAll(async () => {
    process.env.SERVICE_JWT_SECRET = TEST_SERVICE_JWT_SECRET;

    const moduleFixture = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    app.useGlobalPipes(new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }));
    await app.init();

    prisma = app.get(PrismaService);
    serviceToken = generateServiceToken('identity-service');
  });

  beforeEach(async () => {
    // Clean database before each test
    await prisma.shareAccessLog.deleteMany();
    await prisma.backupShare.deleteMany();
  });

  afterAll(async () => {
    await app?.close();
  });

  describe('Complete Workflow', () => {
    it('should complete full lifecycle: store -> retrieve -> revoke', async () => {
      const publicKey = generatePublicKey('x');
      const payload = createStoreSharePayload({
        userId: '50001',
        accountSequence: 50001,
        publicKey,
      });

      // Step 1: Store
      const storeResponse = await request(app.getHttpServer())
        .post('/backup-share/store')
        .set('X-Service-Token', serviceToken)
        .send(payload)
        .expect(201);

      expect(storeResponse.body.success).toBe(true);
      expect(storeResponse.body.shareId).toBeDefined();

      // Step 2: Retrieve
      const retrieveResponse = await request(app.getHttpServer())
        .post('/backup-share/retrieve')
        .set('X-Service-Token', serviceToken)
        .send(createRetrieveSharePayload({ userId: '50001', publicKey }))
        .expect(200);

      expect(retrieveResponse.body.success).toBe(true);
      expect(retrieveResponse.body.partyIndex).toBe(2);

      // Step 3: Revoke
      const revokeResponse = await request(app.getHttpServer())
        .post('/backup-share/revoke')
        .set('X-Service-Token', serviceToken)
        .send(createRevokeSharePayload({ userId: '50001', publicKey, reason: 'ROTATION' }))
        .expect(200);

      expect(revokeResponse.body.success).toBe(true);

      // Step 4: Verify cannot retrieve after revoke
      await request(app.getHttpServer())
        .post('/backup-share/retrieve')
        .set('X-Service-Token', serviceToken)
        .send(createRetrieveSharePayload({ userId: '50001', publicKey }))
        .expect(400);
    });
  });
});
```

---

## Test Utilities

### test-utils.ts

```typescript
// test/utils/test-utils.ts
import jwt from 'jsonwebtoken';

export const TEST_SERVICE_JWT_SECRET = 'test-super-secret-service-jwt-key-for-e2e-testing';
export const TEST_ENCRYPTION_KEY = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
export const TEST_ENCRYPTION_KEY_ID = 'test-key-v1';

// Generate service JWT token
export function generateServiceToken(
  service: string,
  secret: string = TEST_SERVICE_JWT_SECRET,
  expiresIn: string = '1h'
): string {
  return jwt.sign({ service }, secret, { expiresIn });
}

// Generate expired token for testing
export function generateExpiredServiceToken(
  service: string,
  secret: string = TEST_SERVICE_JWT_SECRET
): string {
  return jwt.sign({ service }, secret, { expiresIn: '-1h' });
}

// Generate valid public key (66 chars for compressed)
export function generatePublicKey(prefix: string = 'a'): string {
  return '02' + prefix.repeat(64);
}

// Create store share payload with defaults
export function createStoreSharePayload(overrides: Partial<StoreShareDto> = {}): StoreShareDto {
  return {
    userId: '12345',
    accountSequence: 1001,
    publicKey: generatePublicKey('a'),
    encryptedShareData: 'test-encrypted-share-data',
    ...overrides,
  };
}

// Create retrieve share payload with defaults
export function createRetrieveSharePayload(overrides: Partial<RetrieveShareDto> = {}): RetrieveShareDto {
  return {
    userId: '12345',
    publicKey: generatePublicKey('a'),
    recoveryToken: 'valid-recovery-token',
    ...overrides,
  };
}

// Create revoke share payload with defaults
export function createRevokeSharePayload(overrides: Partial<RevokeShareDto> = {}): RevokeShareDto {
  return {
    userId: '12345',
    publicKey: generatePublicKey('a'),
    reason: 'ROTATION',
    ...overrides,
  };
}
```

### Mock Prisma Service

```typescript
// test/utils/mock-prisma.service.ts
export class MockPrismaService {
  backupShare = {
    create: jest.fn(),
    findUnique: jest.fn(),
    findFirst: jest.fn(),
    update: jest.fn(),
    delete: jest.fn(),
    deleteMany: jest.fn(),
  };

  shareAccessLog = {
    create: jest.fn(),
    findMany: jest.fn(),
    count: jest.fn(),
    deleteMany: jest.fn(),
  };

  $connect = jest.fn();
  $disconnect = jest.fn();
}
```

---

## Running E2E Tests with Real Database

### Using WSL (Windows)

```bash
# 1. Start test database in WSL
wsl docker run -d \
  --name backup-service-test-db \
  -e POSTGRES_USER=postgres \
  -e POSTGRES_PASSWORD=testpassword \
  -e POSTGRES_DB=rwa_backup_test \
  -p 5434:5432 \
  postgres:15-alpine

# 2. Wait for database to be ready
wsl docker logs -f backup-service-test-db

# 3. Run tests (from Windows)
npm run test:e2e:db

# 4. Cleanup
wsl docker stop backup-service-test-db
wsl docker rm backup-service-test-db
```

### Using Docker Compose

```bash
# Start test database
npm run db:test:up

# Run tests
npm run test:e2e:db

# Stop and cleanup
npm run db:test:down
```

### Manual Setup

```bash
# 1. Set environment variables
export DATABASE_URL="postgresql://postgres:testpassword@localhost:5434/rwa_backup_test?schema=public"
export APP_ENV=test
export SERVICE_JWT_SECRET="test-super-secret-service-jwt-key-for-e2e-testing"
export BACKUP_ENCRYPTION_KEY="0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
export BACKUP_ENCRYPTION_KEY_ID="test-key-v1"

# 2. Push schema to test database
npx prisma db push

# 3. Run tests
npm run test:e2e:db
```

---

## Test Coverage

### Current Coverage

| Category | Files | Tests |
|----------|-------|-------|
| Unit Tests | 10 | 37 |
| Mock E2E Tests | 1 | 21 |
| Real DB E2E Tests | 1 | 20 |
| **Total** | **12** | **78** |

### Coverage Report

```bash
# Generate coverage report
npm run test:cov

# View coverage in browser
open coverage/lcov-report/index.html
```

### Coverage Configuration

```json
// In package.json
"jest": {
  "coveragePathIgnorePatterns": [
    "/node_modules/",
    ".module.ts",
    "main.ts"
  ],
  "collectCoverageFrom": [
    "src/**/*.ts",
    "!src/**/*.module.ts",
    "!src/main.ts"
  ]
}
```

---

## Writing Good Tests

### Do's

1. **Test behavior, not implementation**
   ```typescript
   // Good: Tests what the method does
   it('should encrypt share data before storing', async () => {
     await handler.execute(command);
     expect(mockEncryption.encrypt).toHaveBeenCalledWith('original-data');
   });

   // Bad: Tests internal implementation details
   it('should call private method _processData', () => { /* ... */ });
   ```

2. **Use descriptive test names**
   ```typescript
   // Good
   it('should return 401 when service token is missing', () => {});
   it('should increment access count on each retrieve', () => {});

   // Bad
   it('test1', () => {});
   it('works correctly', () => {});
   ```

3. **Arrange-Act-Assert pattern**
   ```typescript
   it('should store backup share successfully', async () => {
     // Arrange
     const command = createStoreCommand();
     mockRepository.findByUserId.mockResolvedValue(null);

     // Act
     const result = await handler.execute(command);

     // Assert
     expect(result.shareId).toBeDefined();
   });
   ```

4. **One assertion per test (when possible)**
   ```typescript
   // Good: Focused tests
   it('should return success true', () => { /* ... */ });
   it('should return the share ID', () => { /* ... */ });

   // Acceptable for E2E: Multiple assertions for a workflow
   it('should complete full lifecycle', () => { /* ... */ });
   ```

### Don'ts

1. **Don't test external libraries**
   ```typescript
   // Bad: Testing that jwt.sign works
   it('should sign JWT token', () => {
     const token = jwt.sign({}, 'secret');
     expect(jwt.verify(token, 'secret')).toBeDefined();
   });
   ```

2. **Don't share state between tests**
   ```typescript
   // Bad: Tests depend on each other
   let sharedData;
   it('test 1', () => { sharedData = 'value'; });
   it('test 2', () => { expect(sharedData).toBe('value'); });
   ```

3. **Don't make tests slow**
   ```typescript
   // Bad: Real network calls
   it('should fetch data', async () => {
     const result = await fetch('https://api.example.com');
   });

   // Good: Mock external services
   it('should fetch data', async () => {
     mockFetch.mockResolvedValue({ data: 'test' });
   });
   ```

---

## CI/CD Integration

### GitHub Actions Example

```yaml
# .github/workflows/test.yml
name: Test

on: [push, pull_request]

jobs:
  unit-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run test:unit

  e2e-test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15-alpine
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: testpassword
          POSTGRES_DB: rwa_backup_test
        ports:
          - 5434:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npx prisma generate
      - run: npx prisma db push
        env:
          DATABASE_URL: postgresql://postgres:testpassword@localhost:5434/rwa_backup_test
      - run: npm run test:e2e:db
        env:
          DATABASE_URL: postgresql://postgres:testpassword@localhost:5434/rwa_backup_test
          SERVICE_JWT_SECRET: test-secret
          BACKUP_ENCRYPTION_KEY: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
          BACKUP_ENCRYPTION_KEY_ID: test-key-v1
```

---

## Debugging Tests

### VSCode Debug Configuration

```json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug Jest Tests",
      "program": "${workspaceFolder}/node_modules/.bin/jest",
      "args": [
        "--runInBand",
        "--no-cache",
        "${fileBasenameNoExtension}"
      ],
      "console": "integratedTerminal"
    }
  ]
}
```

### Running Single Test

```bash
# Run specific test file
npm test -- backup-share.entity.spec.ts

# Run tests matching pattern
npm test -- --testNamePattern="should store"

# Run with verbose output
npm test -- --verbose
```
