810 lines
21 KiB
Markdown
810 lines
21 KiB
Markdown
# 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
|
|
```
|