import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication, ValidationPipe } from '@nestjs/common'; import request from 'supertest'; import { ConfigModule } from '@nestjs/config'; import { ApiModule } from '../../src/api/api.module'; import { ApplicationModule } from '../../src/application/application.module'; import { InfrastructureModule } from '../../src/infrastructure/infrastructure.module'; import { DomainModule } from '../../src/domain/domain.module'; import { PrismaService } from '../../src/infrastructure/persistence/prisma/prisma.service'; import { GlobalExceptionFilter } from '../../src/shared/filters/global-exception.filter'; import { MockPrismaService } from '../utils/mock-prisma.service'; import { generateServiceToken, generatePublicKey, createStoreSharePayload, createRetrieveSharePayload, createRevokeSharePayload, setupTestEnv, TEST_SERVICE_JWT_SECRET, } from '../utils/test-utils'; describe('BackupShare E2E (Mock Database)', () => { let app: INestApplication; let mockPrisma: MockPrismaService; let serviceToken: string; beforeAll(async () => { setupTestEnv(); mockPrisma = new MockPrismaService(); const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ isGlobal: true, load: [ () => ({ app: { port: 3002, env: 'test' }, security: { serviceJwtSecret: TEST_SERVICE_JWT_SECRET, allowedServices: ['identity-service', 'recovery-service'], }, encryption: { key: process.env.BACKUP_ENCRYPTION_KEY, keyId: process.env.BACKUP_ENCRYPTION_KEY_ID, }, rateLimit: { maxRetrievePerDay: 3 }, }), ], }), DomainModule, InfrastructureModule, ApplicationModule, ApiModule, ], }) .overrideProvider(PrismaService) .useValue(mockPrisma) .compile(); app = moduleFixture.createNestApplication(); app.useGlobalPipes( new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, }), ); app.useGlobalFilters(new GlobalExceptionFilter()); await app.init(); serviceToken = generateServiceToken('identity-service'); }); beforeEach(() => { mockPrisma.reset(); }); afterAll(async () => { await app?.close(); }); describe('Complete Workflow Tests', () => { it('should complete full lifecycle: store -> retrieve -> revoke', async () => { const publicKey = generatePublicKey('x'); const storePayload = createStoreSharePayload({ userId: '11111', accountSequence: 1111, publicKey, }); // Step 1: Store const storeResponse = await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send(storePayload) .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: '11111', 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: '11111', 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: '11111', publicKey })) .expect(400); }); it('should allow recovery-service to access shares', async () => { const recoveryToken = generateServiceToken('recovery-service'); const publicKey = generatePublicKey('r'); // Store with identity-service await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send(createStoreSharePayload({ userId: '22222', accountSequence: 2222, publicKey })) .expect(201); // Retrieve with recovery-service const response = await request(app.getHttpServer()) .post('/backup-share/retrieve') .set('X-Service-Token', recoveryToken) .send(createRetrieveSharePayload({ userId: '22222', publicKey })) .expect(200); expect(response.body.success).toBe(true); }); }); describe('Validation Tests', () => { it('should reject store with missing userId', async () => { const payload = { ...createStoreSharePayload(), userId: undefined }; delete (payload as any).userId; await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send(payload) .expect(400); }); it('should reject store with missing accountSequence', async () => { const payload = { ...createStoreSharePayload(), accountSequence: undefined }; delete (payload as any).accountSequence; await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send(payload) .expect(400); }); it('should reject store with invalid public key length', async () => { await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send(createStoreSharePayload({ publicKey: 'too-short' })) .expect(400); }); it('should reject retrieve with missing recoveryToken', async () => { const payload = { ...createRetrieveSharePayload(), recoveryToken: undefined }; delete (payload as any).recoveryToken; await request(app.getHttpServer()) .post('/backup-share/retrieve') .set('X-Service-Token', serviceToken) .send(payload) .expect(400); }); it('should reject revoke with invalid reason', async () => { await request(app.getHttpServer()) .post('/backup-share/revoke') .set('X-Service-Token', serviceToken) .send(createRevokeSharePayload({ reason: 'INVALID_REASON' })) .expect(400); }); it('should accept all valid revoke reasons', async () => { const validReasons = ['ROTATION', 'ACCOUNT_CLOSED', 'SECURITY_BREACH', 'USER_REQUEST']; for (let i = 0; i < validReasons.length; i++) { const publicKey = generatePublicKey(String(i)); const userId = String(30000 + i); // Store first await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send(createStoreSharePayload({ userId, accountSequence: 30000 + i, publicKey })) .expect(201); // Then revoke with valid reason await request(app.getHttpServer()) .post('/backup-share/revoke') .set('X-Service-Token', serviceToken) .send(createRevokeSharePayload({ userId, publicKey, reason: validReasons[i] })) .expect(200); } }); }); describe('Authentication Tests', () => { it('should reject requests without token', async () => { await request(app.getHttpServer()) .post('/backup-share/store') .send(createStoreSharePayload()) .expect(401); await request(app.getHttpServer()) .post('/backup-share/retrieve') .send(createRetrieveSharePayload()) .expect(401); await request(app.getHttpServer()) .post('/backup-share/revoke') .send(createRevokeSharePayload()) .expect(401); }); it('should reject requests with malformed token', async () => { await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', 'not.a.valid.jwt') .send(createStoreSharePayload()) .expect(401); }); it('should reject requests from unknown services', async () => { const unknownToken = generateServiceToken('malicious-service'); await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', unknownToken) .send(createStoreSharePayload()) .expect(401); }); it('should reject tokens signed with wrong secret', async () => { const badToken = generateServiceToken('identity-service', 'wrong-secret'); await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', badToken) .send(createStoreSharePayload()) .expect(401); }); }); describe('Error Handling Tests', () => { it('should return SHARE_NOT_FOUND for non-existent share', async () => { const response = await request(app.getHttpServer()) .post('/backup-share/retrieve') .set('X-Service-Token', serviceToken) .send(createRetrieveSharePayload({ userId: '99999999', publicKey: generatePublicKey('z') })) .expect(404); expect(response.body.code).toBe('SHARE_NOT_FOUND'); }); it('should return SHARE_ALREADY_EXISTS for duplicate store', async () => { const publicKey = generatePublicKey('d'); const payload = createStoreSharePayload({ userId: '44444', accountSequence: 4444, publicKey }); await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send(payload) .expect(201); const response = await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send(payload) .expect(409); expect(response.body.code).toBe('SHARE_ALREADY_EXISTS'); }); it('should return SHARE_NOT_ACTIVE for revoked share retrieval', async () => { const publicKey = generatePublicKey('e'); await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send(createStoreSharePayload({ userId: '55555', accountSequence: 5555, publicKey })) .expect(201); await request(app.getHttpServer()) .post('/backup-share/revoke') .set('X-Service-Token', serviceToken) .send(createRevokeSharePayload({ userId: '55555', publicKey })) .expect(200); const response = await request(app.getHttpServer()) .post('/backup-share/retrieve') .set('X-Service-Token', serviceToken) .send(createRetrieveSharePayload({ userId: '55555', publicKey })) .expect(400); expect(response.body.code).toBe('SHARE_NOT_ACTIVE'); }); }); describe('Optional Parameters Tests', () => { it('should accept custom threshold and totalParties', async () => { const response = await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send(createStoreSharePayload({ userId: '66666', accountSequence: 6666, publicKey: generatePublicKey('f'), threshold: 3, totalParties: 5, })) .expect(201); expect(response.body.success).toBe(true); }); it('should accept deviceId in retrieve', async () => { const publicKey = generatePublicKey('g'); await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send(createStoreSharePayload({ userId: '77777', accountSequence: 7777, publicKey })) .expect(201); const response = await request(app.getHttpServer()) .post('/backup-share/retrieve') .set('X-Service-Token', serviceToken) .send(createRetrieveSharePayload({ userId: '77777', publicKey, deviceId: 'device-abc-123' })) .expect(200); expect(response.body.success).toBe(true); }); }); describe('Response Structure Tests', () => { it('should return correct store response structure', async () => { const response = await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send(createStoreSharePayload({ userId: '80001', accountSequence: 80001, publicKey: generatePublicKey('s'), })) .expect(201); expect(response.body).toHaveProperty('success', true); expect(response.body).toHaveProperty('shareId'); expect(response.body).toHaveProperty('message', 'Backup share stored successfully'); }); it('should return correct retrieve response structure', async () => { const publicKey = generatePublicKey('t'); await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send(createStoreSharePayload({ userId: '80002', accountSequence: 80002, publicKey })) .expect(201); const response = await request(app.getHttpServer()) .post('/backup-share/retrieve') .set('X-Service-Token', serviceToken) .send(createRetrieveSharePayload({ userId: '80002', publicKey })) .expect(200); expect(response.body).toHaveProperty('success', true); expect(response.body).toHaveProperty('encryptedShareData'); expect(response.body).toHaveProperty('partyIndex', 2); expect(response.body).toHaveProperty('publicKey', publicKey); }); it('should return correct revoke response structure', async () => { const publicKey = generatePublicKey('u'); await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send(createStoreSharePayload({ userId: '80003', accountSequence: 80003, publicKey })) .expect(201); const response = await request(app.getHttpServer()) .post('/backup-share/revoke') .set('X-Service-Token', serviceToken) .send(createRevokeSharePayload({ userId: '80003', publicKey })) .expect(200); expect(response.body).toHaveProperty('success', true); expect(response.body).toHaveProperty('message', 'Backup share revoked successfully'); }); it('should return correct error response structure', async () => { const response = await request(app.getHttpServer()) .post('/backup-share/retrieve') .set('X-Service-Token', serviceToken) .send(createRetrieveSharePayload({ userId: '99999', publicKey: generatePublicKey('v') })) .expect(404); expect(response.body).toHaveProperty('success', false); expect(response.body).toHaveProperty('error'); expect(response.body).toHaveProperty('code'); expect(response.body).toHaveProperty('timestamp'); expect(response.body).toHaveProperty('path'); }); }); });