diff --git a/infra/k8s/base/monitoring/alert-rules-configmap.yaml b/infra/k8s/base/monitoring/alert-rules-configmap.yaml new file mode 100644 index 0000000..987d3e7 --- /dev/null +++ b/infra/k8s/base/monitoring/alert-rules-configmap.yaml @@ -0,0 +1,86 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: alert-rules + namespace: the-order +data: + alert-rules.yml: | + # Prometheus Alert Rules + # Defines alerting conditions for The Order services + + groups: + - name: service_health + interval: 30s + rules: + - alert: ServiceDown + expr: up{job=~"identity-service|intake-service|finance-service|dataroom-service|legal-documents-service"} == 0 + for: 5m + labels: + severity: critical + annotations: + summary: "Service {{ $labels.job }} is down" + description: "Service {{ $labels.job }} has been down for more than 5 minutes" + + - alert: HighErrorRate + expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.05 + for: 5m + labels: + severity: warning + annotations: + summary: "High error rate for {{ $labels.job }}" + description: "Error rate is {{ $value }} errors per second" + + - alert: HighResponseTime + expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 2 + for: 10m + labels: + severity: warning + annotations: + summary: "High response time for {{ $labels.job }}" + description: "95th percentile response time is {{ $value }} seconds" + + - name: infrastructure + interval: 30s + rules: + - alert: HighCPUUsage + expr: rate(process_cpu_user_seconds_total[5m]) > 0.8 + for: 10m + labels: + severity: warning + annotations: + summary: "High CPU usage for {{ $labels.job }}" + description: "CPU usage is {{ $value }}%" + + - alert: HighMemoryUsage + expr: (process_resident_memory_bytes / process_virtual_memory_bytes) > 0.9 + for: 10m + labels: + severity: warning + annotations: + summary: "High memory usage for {{ $labels.job }}" + description: "Memory usage is {{ $value }}%" + + - name: database + interval: 30s + rules: + - alert: DatabaseConnectionPoolExhausted + expr: pg_stat_database_numbackends / pg_settings_max_connections > 0.8 + for: 5m + labels: + severity: warning + annotations: + summary: "Database connection pool nearly exhausted" + description: "{{ $value }}% of connections in use" + + - name: azure + interval: 30s + rules: + - alert: EntraAPIRateLimit + expr: rate(entra_api_requests_total{status="429"}[5m]) > 0 + for: 1m + labels: + severity: warning + annotations: + summary: "Entra API rate limit hit" + description: "Rate limit errors detected for Entra VerifiedID API" + diff --git a/infra/k8s/base/monitoring/prometheus-configmap.yaml b/infra/k8s/base/monitoring/prometheus-configmap.yaml new file mode 100644 index 0000000..ee20397 --- /dev/null +++ b/infra/k8s/base/monitoring/prometheus-configmap.yaml @@ -0,0 +1,97 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: prometheus-config + namespace: the-order +data: + prometheus.yml: | + global: + scrape_interval: 15s + evaluation_interval: 15s + external_labels: + cluster: 'the-order' + environment: 'production' + + scrape_configs: + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + + - job_name: 'identity-service' + kubernetes_sd_configs: + - role: pod + namespaces: + names: + - the-order + relabel_configs: + - source_labels: [__meta_kubernetes_pod_label_app] + regex: identity-service + action: keep + - source_labels: [__meta_kubernetes_pod_ip] + action: replace + target_label: __address__ + replacement: $1:4002 + + - job_name: 'intake-service' + kubernetes_sd_configs: + - role: pod + namespaces: + names: + - the-order + relabel_configs: + - source_labels: [__meta_kubernetes_pod_label_app] + regex: intake-service + action: keep + - source_labels: [__meta_kubernetes_pod_ip] + action: replace + target_label: __address__ + replacement: $1:4001 + + - job_name: 'finance-service' + kubernetes_sd_configs: + - role: pod + namespaces: + names: + - the-order + relabel_configs: + - source_labels: [__meta_kubernetes_pod_label_app] + regex: finance-service + action: keep + - source_labels: [__meta_kubernetes_pod_ip] + action: replace + target_label: __address__ + replacement: $1:4003 + + - job_name: 'dataroom-service' + kubernetes_sd_configs: + - role: pod + namespaces: + names: + - the-order + relabel_configs: + - source_labels: [__meta_kubernetes_pod_label_app] + regex: dataroom-service + action: keep + - source_labels: [__meta_kubernetes_pod_ip] + action: replace + target_label: __address__ + replacement: $1:4004 + + - job_name: 'legal-documents-service' + kubernetes_sd_configs: + - role: pod + namespaces: + names: + - the-order + relabel_configs: + - source_labels: [__meta_kubernetes_pod_label_app] + regex: legal-documents-service + action: keep + - source_labels: [__meta_kubernetes_pod_ip] + action: replace + target_label: __address__ + replacement: $1:4005 + + rule_files: + - '/etc/prometheus/alert-rules.yml' + diff --git a/infra/k8s/base/monitoring/prometheus-deployment.yaml b/infra/k8s/base/monitoring/prometheus-deployment.yaml index 71a901e..048a10d 100644 --- a/infra/k8s/base/monitoring/prometheus-deployment.yaml +++ b/infra/k8s/base/monitoring/prometheus-deployment.yaml @@ -27,6 +27,9 @@ spec: volumeMounts: - name: prometheus-config mountPath: /etc/prometheus + - name: alert-rules + mountPath: /etc/prometheus/alert-rules.yml + subPath: alert-rules.yml - name: prometheus-storage mountPath: /prometheus resources: diff --git a/infra/monitoring/alert-rules.yml b/infra/monitoring/alert-rules.yml index ac19a95..352b2b9 100644 --- a/infra/monitoring/alert-rules.yml +++ b/infra/monitoring/alert-rules.yml @@ -1,9 +1,12 @@ +# Prometheus Alert Rules +# Defines alerting conditions for The Order services + groups: - name: service_health interval: 30s rules: - alert: ServiceDown - expr: up{job=~".*-service"} == 0 + expr: up{job=~"identity-service|intake-service|finance-service|dataroom-service|legal-documents-service"} == 0 for: 5m labels: severity: critical @@ -17,7 +20,7 @@ groups: labels: severity: warning annotations: - summary: "High error rate in {{ $labels.job }}" + summary: "High error rate for {{ $labels.job }}" description: "Error rate is {{ $value }} errors per second" - alert: HighResponseTime @@ -26,52 +29,52 @@ groups: labels: severity: warning annotations: - summary: "High response time in {{ $labels.job }}" + summary: "High response time for {{ $labels.job }}" description: "95th percentile response time is {{ $value }} seconds" - - name: resource_usage + - name: infrastructure interval: 30s rules: - alert: HighCPUUsage - expr: rate(container_cpu_usage_seconds_total[5m]) > 0.8 + expr: rate(process_cpu_user_seconds_total[5m]) > 0.8 for: 10m labels: severity: warning annotations: - summary: "High CPU usage in {{ $labels.pod }}" - description: "CPU usage is {{ $value }}" + summary: "High CPU usage for {{ $labels.job }}" + description: "CPU usage is {{ $value }}%" - alert: HighMemoryUsage - expr: container_memory_usage_bytes / container_spec_memory_limit_bytes > 0.9 + expr: (process_resident_memory_bytes / process_virtual_memory_bytes) > 0.9 for: 10m labels: severity: warning annotations: - summary: "High memory usage in {{ $labels.pod }}" + summary: "High memory usage for {{ $labels.job }}" description: "Memory usage is {{ $value }}%" - - alert: PodCrashLooping - expr: rate(kube_pod_container_status_restarts_total[15m]) > 0 + - alert: DiskSpaceLow + expr: (node_filesystem_avail_bytes / node_filesystem_size_bytes) < 0.1 for: 5m labels: severity: critical annotations: - summary: "Pod {{ $labels.pod }} is crash looping" - description: "Pod has restarted {{ $value }} times in the last 15 minutes" + summary: "Low disk space on {{ $labels.instance }}" + description: "Disk space is {{ $value }}% available" - name: database interval: 30s rules: - - alert: DatabaseConnectionHigh - expr: pg_stat_database_numbackends / pg_stat_database_max_connections > 0.8 + - alert: DatabaseConnectionPoolExhausted + expr: pg_stat_database_numbackends / pg_settings_max_connections > 0.8 for: 5m labels: severity: warning annotations: - summary: "High database connection usage" - description: "{{ $value }}% of max connections in use" + summary: "Database connection pool nearly exhausted" + description: "{{ $value }}% of connections in use" - - alert: DatabaseSlowQueries + - alert: SlowQueries expr: rate(pg_stat_statements_mean_exec_time[5m]) > 1 for: 10m labels: @@ -80,24 +83,23 @@ groups: summary: "Slow database queries detected" description: "Average query time is {{ $value }} seconds" - - name: entra_verifiedid + - name: azure interval: 30s rules: - - alert: EntraAPIFailure - expr: rate(entra_api_errors_total[5m]) > 0.1 - for: 5m + - alert: EntraAPIRateLimit + expr: rate(entra_api_requests_total{status="429"}[5m]) > 0 + for: 1m labels: - severity: critical + severity: warning annotations: - summary: "High Entra VerifiedID API error rate" - description: "Error rate is {{ $value }} errors per second" + summary: "Entra API rate limit hit" + description: "Rate limit errors detected for Entra VerifiedID API" - - alert: EntraRateLimitApproaching - expr: entra_rate_limit_remaining / entra_rate_limit_total < 0.1 + - alert: AzureStorageErrors + expr: rate(azure_storage_errors_total[5m]) > 0.01 for: 5m labels: severity: warning annotations: - summary: "Entra VerifiedID rate limit approaching" - description: "Only {{ $value }}% of rate limit remaining" - + summary: "Azure Storage errors detected" + description: "Storage error rate is {{ $value }} errors per second" diff --git a/infra/monitoring/prometheus-config.yml b/infra/monitoring/prometheus-config.yml index 3c91e77..c3233f4 100644 --- a/infra/monitoring/prometheus-config.yml +++ b/infra/monitoring/prometheus-config.yml @@ -138,5 +138,5 @@ alerting: - alertmanager:9093 rule_files: - - '/etc/prometheus/alerts/*.yml' + - '/etc/prometheus/alert-rules.yml' diff --git a/packages/test-utils/src/server-factory.ts b/packages/test-utils/src/server-factory.ts new file mode 100644 index 0000000..bce7f4b --- /dev/null +++ b/packages/test-utils/src/server-factory.ts @@ -0,0 +1,44 @@ +/** + * Server Factory for Testing + * Creates Fastify server instances for testing + */ + +import { FastifyInstance } from 'fastify'; +import Fastify from 'fastify'; +import { getEnv } from '@the-order/shared'; + +export interface ServerFactoryOptions { + port?: number; + logger?: boolean; + [key: string]: unknown; +} + +export async function createTestServer( + routes: (server: FastifyInstance) => Promise, + options: ServerFactoryOptions = {} +): Promise { + const server = Fastify({ + logger: options.logger ?? false, + requestIdLogLabel: 'requestId', + disableRequestLogging: !options.logger, + }); + + // Register routes + await routes(server); + + // Health check + server.get('/health', () => { + return { status: 'ok' }; + }); + + if (options.port) { + await server.listen({ port: options.port, host: '0.0.0.0' }); + } + + return server; +} + +export function closeTestServer(server: FastifyInstance): Promise { + return server.close(); +} + diff --git a/services/dataroom/tests/dataroom.test.ts b/services/dataroom/tests/dataroom.test.ts new file mode 100644 index 0000000..6d79804 --- /dev/null +++ b/services/dataroom/tests/dataroom.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { FastifyInstance } from 'fastify'; +import { createServer } from '../src/index'; + +describe('Dataroom Service', () => { + let server: FastifyInstance; + + beforeEach(async () => { + server = await createServer(); + await server.ready(); + }); + + afterEach(async () => { + await server.close(); + }); + + describe('Health Check', () => { + it('should return 200 on health check', async () => { + const response = await server.inject({ + method: 'GET', + url: '/health', + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toMatchObject({ + status: 'ok', + }); + }); + }); + + describe('Deal Management', () => { + it('should validate deal creation request', async () => { + const response = await server.inject({ + method: 'POST', + url: '/api/v1/deals', + payload: { + // Invalid payload to test validation + }, + }); + + expect(response.statusCode).toBe(400); + }); + }); +}); + diff --git a/services/dataroom/tsconfig.json b/services/dataroom/tsconfig.json index a0abf59..3c7e1c2 100644 --- a/services/dataroom/tsconfig.json +++ b/services/dataroom/tsconfig.json @@ -5,7 +5,7 @@ "rootDir": "./src", "composite": true }, - "include": ["src/**/*"], + "include": ["src/**/*", "tests/**/*"], "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"], "references": [ { "path": "../../packages/shared" }, diff --git a/services/finance/tests/finance.test.ts b/services/finance/tests/finance.test.ts new file mode 100644 index 0000000..2c815ab --- /dev/null +++ b/services/finance/tests/finance.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { FastifyInstance } from 'fastify'; +import { createServer } from '../src/index'; + +describe('Finance Service', () => { + let server: FastifyInstance; + + beforeEach(async () => { + server = await createServer(); + await server.ready(); + }); + + afterEach(async () => { + await server.close(); + }); + + describe('Health Check', () => { + it('should return 200 on health check', async () => { + const response = await server.inject({ + method: 'GET', + url: '/health', + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toMatchObject({ + status: 'ok', + }); + }); + }); + + describe('Payment Processing', () => { + it('should validate payment request schema', async () => { + const response = await server.inject({ + method: 'POST', + url: '/api/v1/payments', + payload: { + // Invalid payload to test validation + }, + }); + + expect(response.statusCode).toBe(400); + }); + }); +}); + diff --git a/services/finance/tsconfig.json b/services/finance/tsconfig.json index ffcc78f..007539a 100644 --- a/services/finance/tsconfig.json +++ b/services/finance/tsconfig.json @@ -5,7 +5,7 @@ "rootDir": "./src", "composite": true }, - "include": ["src/**/*"], + "include": ["src/**/*", "tests/**/*"], "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"], "references": [ { "path": "../../packages/shared" }, diff --git a/services/identity/tests/identity.test.ts b/services/identity/tests/identity.test.ts new file mode 100644 index 0000000..168fce9 --- /dev/null +++ b/services/identity/tests/identity.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { FastifyInstance } from 'fastify'; +import { createServer } from '../src/index'; + +describe('Identity Service', () => { + let server: FastifyInstance; + + beforeEach(async () => { + server = await createServer(); + await server.ready(); + }); + + afterEach(async () => { + await server.close(); + }); + + describe('Health Check', () => { + it('should return 200 on health check', async () => { + const response = await server.inject({ + method: 'GET', + url: '/health', + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toMatchObject({ + status: 'ok', + }); + }); + }); + + describe('Credential Issuance', () => { + it('should validate credential request schema', async () => { + const response = await server.inject({ + method: 'POST', + url: '/api/v1/credentials/issue', + payload: { + // Invalid payload to test validation + }, + }); + + expect(response.statusCode).toBe(400); + }); + }); +}); + diff --git a/services/identity/tsconfig.json b/services/identity/tsconfig.json index be980e2..ec98cf0 100644 --- a/services/identity/tsconfig.json +++ b/services/identity/tsconfig.json @@ -4,7 +4,7 @@ "outDir": "./dist", "composite": true }, - "include": ["src/**/*"], + "include": ["src/**/*", "tests/**/*"], "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"], "references": [ { "path": "../../packages/shared" }, diff --git a/services/intake/tests/intake.test.ts b/services/intake/tests/intake.test.ts new file mode 100644 index 0000000..60f2c18 --- /dev/null +++ b/services/intake/tests/intake.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { FastifyInstance } from 'fastify'; +import { createServer } from '../src/index'; + +describe('Intake Service', () => { + let server: FastifyInstance; + + beforeEach(async () => { + server = await createServer(); + await server.ready(); + }); + + afterEach(async () => { + await server.close(); + }); + + describe('Health Check', () => { + it('should return 200 on health check', async () => { + const response = await server.inject({ + method: 'GET', + url: '/health', + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toMatchObject({ + status: 'ok', + }); + }); + }); + + describe('Document Upload', () => { + it('should validate document upload request', async () => { + const response = await server.inject({ + method: 'POST', + url: '/api/v1/documents', + payload: { + // Invalid payload to test validation + }, + }); + + expect(response.statusCode).toBe(400); + }); + }); +}); + diff --git a/services/intake/tsconfig.json b/services/intake/tsconfig.json index e4612ee..b8ce287 100644 --- a/services/intake/tsconfig.json +++ b/services/intake/tsconfig.json @@ -5,7 +5,7 @@ "rootDir": "./src", "composite": true }, - "include": ["src/**/*"], + "include": ["src/**/*", "tests/**/*"], "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"], "references": [ { "path": "../../packages/shared" }, diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..6166119 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,91 @@ +# Testing Documentation + +**Last Updated**: 2025-01-27 +**Status**: Test Framework Setup + +## Test Structure + +``` +tests/ +├── integration/ # Integration tests +│ ├── setup.ts # Test context setup +│ ├── identity-credential-flow.test.ts +│ └── document-workflow.test.ts +└── e2e/ # End-to-end tests + └── user-workflows.test.ts +``` + +## Running Tests + +### All Tests +```bash +pnpm test +``` + +### Unit Tests Only +```bash +pnpm test -- --run unit +``` + +### Integration Tests +```bash +pnpm test -- --run integration +``` + +### E2E Tests +```bash +pnpm test -- --run e2e +``` + +### With Coverage +```bash +pnpm test -- --coverage +``` + +## Test Coverage Goals + +- **Target**: 80%+ coverage across all services +- **Current**: Expanding coverage +- **Priority**: Critical service paths first + +## Test Types + +### Unit Tests +- Service-specific tests in `services/*/tests/` +- Test individual functions and modules +- Mock external dependencies + +### Integration Tests +- Test service interactions +- Use test database +- Test API endpoints + +### E2E Tests +- Test complete user workflows +- Test across multiple services +- Test real-world scenarios + +## Test Utilities + +### Test Context +- `setupTestContext()` - Initialize all services +- `teardownTestContext()` - Clean up services +- `cleanupDatabase()` - Clean test data + +### Fixtures +- Test data factories +- Mock services +- Test helpers + +## Best Practices + +1. **Isolation**: Each test should be independent +2. **Cleanup**: Always clean up test data +3. **Mocking**: Mock external services +4. **Coverage**: Aim for 80%+ coverage +5. **Speed**: Keep tests fast + +--- + +**Last Updated**: 2025-01-27 + diff --git a/tests/e2e/user-workflows.test.ts b/tests/e2e/user-workflows.test.ts new file mode 100644 index 0000000..002823f --- /dev/null +++ b/tests/e2e/user-workflows.test.ts @@ -0,0 +1,85 @@ +/** + * End-to-End Tests: User Workflows + * Tests complete user workflows across multiple services + */ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { setupTestContext, teardownTestContext, cleanupDatabase, TestContext } from '../integration/setup'; + +describe('User Workflows - E2E', () => { + let context: TestContext; + + beforeAll(async () => { + context = await setupTestContext(); + await cleanupDatabase(context.dbPool); + }); + + afterAll(async () => { + await cleanupDatabase(context.dbPool); + await teardownTestContext(context); + }); + + describe('Member Onboarding Workflow', () => { + it('should complete full member onboarding flow', async () => { + // 1. Create identity + const identityResponse = await context.identityService.inject({ + method: 'POST', + url: '/api/v1/identities', + payload: { + did: 'did:example:member123', + eidasLevel: 'substantial', + }, + }); + + expect(identityResponse.statusCode).toBe(201); + const identity = identityResponse.json(); + + // 2. Issue membership credential + const credentialResponse = await context.identityService.inject({ + method: 'POST', + url: '/api/v1/credentials/issue', + payload: { + identityId: identity.id, + credentialType: 'membership', + claims: { + name: 'John Doe', + membershipNumber: 'MEMBER-001', + }, + }, + }); + + expect(credentialResponse.statusCode).toBe(201); + const credential = credentialResponse.json(); + + // 3. Create initial payment + const paymentResponse = await context.financeService.inject({ + method: 'POST', + url: '/api/v1/payments', + payload: { + amount: 10000, // $100.00 + currency: 'USD', + description: 'Membership fee', + // Add payment method + }, + }); + + expect(paymentResponse.statusCode).toBe(201); + + // Verify complete workflow + expect(identity).toHaveProperty('id'); + expect(credential).toHaveProperty('id'); + }); + }); + + describe('Document Management Workflow', () => { + it('should handle complete document lifecycle', async () => { + // 1. Upload document + // 2. Process through OCR + // 3. Classify document + // 4. Store in dataroom + // 5. Grant access + // Implementation depends on service APIs + }); + }); +}); + diff --git a/tests/integration/document-workflow.test.ts b/tests/integration/document-workflow.test.ts new file mode 100644 index 0000000..bd8a9e7 --- /dev/null +++ b/tests/integration/document-workflow.test.ts @@ -0,0 +1,61 @@ +/** + * Integration Test: Document Workflow + * Tests document creation, versioning, and workflow + */ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { setupTestContext, teardownTestContext, cleanupDatabase, TestContext } from './setup'; + +describe('Document Workflow - Integration', () => { + let context: TestContext; + + beforeAll(async () => { + context = await setupTestContext(); + await cleanupDatabase(context.dbPool); + }); + + afterAll(async () => { + await cleanupDatabase(context.dbPool); + await teardownTestContext(context); + }); + + describe('Document Lifecycle', () => { + it('should create, update, and version a document', async () => { + // 1. Create document via intake service + const createResponse = await context.intakeService.inject({ + method: 'POST', + url: '/api/v1/documents', + payload: { + title: 'Test Document', + contentType: 'application/pdf', + // Add other required fields + }, + }); + + expect(createResponse.statusCode).toBe(201); + const document = createResponse.json(); + + // 2. Update document + const updateResponse = await context.intakeService.inject({ + method: 'PATCH', + url: `/api/v1/documents/${document.id}`, + payload: { + title: 'Updated Test Document', + }, + }); + + expect(updateResponse.statusCode).toBe(200); + + // 3. Check version history + const versionsResponse = await context.intakeService.inject({ + method: 'GET', + url: `/api/v1/documents/${document.id}/versions`, + }); + + expect(versionsResponse.statusCode).toBe(200); + const versions = versionsResponse.json(); + expect(versions).toHaveLength(2); // Original + update + }); + }); +}); + diff --git a/tests/integration/identity-credential-flow.test.ts b/tests/integration/identity-credential-flow.test.ts new file mode 100644 index 0000000..7b62964 --- /dev/null +++ b/tests/integration/identity-credential-flow.test.ts @@ -0,0 +1,63 @@ +/** + * Integration Test: Identity Credential Flow + * Tests the complete flow of credential issuance and verification + */ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { setupTestContext, teardownTestContext, cleanupDatabase, TestContext } from './setup'; + +describe('Identity Credential Flow - Integration', () => { + let context: TestContext; + + beforeAll(async () => { + context = await setupTestContext(); + await cleanupDatabase(context.dbPool); + }); + + afterAll(async () => { + await cleanupDatabase(context.dbPool); + await teardownTestContext(context); + }); + + describe('Credential Issuance Flow', () => { + it('should issue a verifiable credential end-to-end', async () => { + // 1. Create identity + const identityResponse = await context.identityService.inject({ + method: 'POST', + url: '/api/v1/identities', + payload: { + did: 'did:example:123', + eidasLevel: 'substantial', + }, + }); + + expect(identityResponse.statusCode).toBe(201); + const identity = identityResponse.json(); + + // 2. Issue credential + const credentialResponse = await context.identityService.inject({ + method: 'POST', + url: '/api/v1/credentials/issue', + payload: { + identityId: identity.id, + credentialType: 'membership', + claims: { + name: 'Test User', + membershipNumber: '12345', + }, + }, + }); + + expect(credentialResponse.statusCode).toBe(201); + const credential = credentialResponse.json(); + expect(credential).toHaveProperty('id'); + expect(credential).toHaveProperty('credentialType', 'membership'); + }); + + it('should verify a credential', async () => { + // This would test credential verification + // Implementation depends on verifier-sdk + }); + }); +}); + diff --git a/tests/integration/setup.ts b/tests/integration/setup.ts new file mode 100644 index 0000000..c95d319 --- /dev/null +++ b/tests/integration/setup.ts @@ -0,0 +1,64 @@ +/** + * Integration Test Setup + * Provides shared utilities and fixtures for integration tests + */ + +import { FastifyInstance } from 'fastify'; +import { getPool } from '@the-order/database'; + +export interface TestContext { + identityService: FastifyInstance; + intakeService: FastifyInstance; + financeService: FastifyInstance; + dataroomService: FastifyInstance; + dbPool: ReturnType; +} + +export async function setupTestContext(): Promise { + // Import services dynamically to avoid circular dependencies + const { createServer: createIdentityServer } = await import('../../services/identity/src/index'); + const { createServer: createIntakeServer } = await import('../../services/intake/src/index'); + const { createServer: createFinanceServer } = await import('../../services/finance/src/index'); + const { createServer: createDataroomServer } = await import('../../services/dataroom/src/index'); + + const identityService = await createIdentityServer(); + const intakeService = await createIntakeServer(); + const financeService = await createFinanceServer(); + const dataroomService = await createDataroomServer(); + + await Promise.all([ + identityService.ready(), + intakeService.ready(), + financeService.ready(), + dataroomService.ready(), + ]); + + const dbPool = getPool({ + connectionString: process.env.TEST_DATABASE_URL || process.env.DATABASE_URL || '', + }); + + return { + identityService, + intakeService, + financeService, + dataroomService, + dbPool, + }; +} + +export async function teardownTestContext(context: TestContext): Promise { + await Promise.all([ + context.identityService.close(), + context.intakeService.close(), + context.financeService.close(), + context.dataroomService.close(), + ]); + + await context.dbPool.end(); +} + +export async function cleanupDatabase(pool: ReturnType): Promise { + // Clean up test data + await pool.query('TRUNCATE TABLE credentials, documents, payments, deals CASCADE'); +} + diff --git a/tests/tsconfig.json b/tests/tsconfig.json new file mode 100644 index 0000000..0219270 --- /dev/null +++ b/tests/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": ".", + "types": ["vitest/globals", "node"] + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist"] +} +