import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication, ValidationPipe } from '@nestjs/common'; import request from 'supertest'; import { AppModule } from '../../src/app.module'; import { PrismaService } from '../../src/infrastructure/persistence/prisma/prisma.service'; import { generateServiceToken, generatePublicKey, createStoreSharePayload, createRetrieveSharePayload, createRevokeSharePayload, TEST_SERVICE_JWT_SECRET, } from '../utils/test-utils'; describe('BackupShare E2E (Real Database)', () => { let app: INestApplication; let prisma: PrismaService; let serviceToken: string; beforeAll(async () => { // Environment variables are loaded by jest-e2e-setup.ts process.env.SERVICE_JWT_SECRET = TEST_SERVICE_JWT_SECRET; const moduleFixture: TestingModule = 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 if (prisma) { await prisma.shareAccessLog.deleteMany(); await prisma.backupShare.deleteMany(); } }); afterAll(async () => { await app?.close(); }); describe('Health Check', () => { it('GET /health should return ok', () => { return request(app.getHttpServer()) .get('/health') .expect(200) .expect((res) => { expect(res.body.status).toBe('ok'); expect(res.body.service).toBe('backup-service'); }); }); it('GET /health/live should return alive', () => { return request(app.getHttpServer()) .get('/health/live') .expect(200) .expect((res) => { expect(res.body.status).toBe('alive'); }); }); }); describe('POST /backup-share/store', () => { it('should store backup share successfully', async () => { const payload = createStoreSharePayload({ userId: '10001', accountSequence: 10001, publicKey: generatePublicKey('a'), }); const response = await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send(payload) .expect(201); expect(response.body.success).toBe(true); expect(response.body.shareId).toBeDefined(); expect(response.body.message).toBe('Backup share stored successfully'); // Verify data persisted to database const share = await prisma.backupShare.findUnique({ where: { userId: BigInt(payload.userId) }, }); expect(share).not.toBeNull(); expect(share?.publicKey).toBe(payload.publicKey); expect(share?.status).toBe('ACTIVE'); }); it('should reject duplicate share for same user', async () => { const payload = createStoreSharePayload({ userId: '10002', accountSequence: 10002, publicKey: generatePublicKey('b'), }); // First store await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send(payload) .expect(201); // Duplicate attempt const response = await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send(payload) .expect(409); expect(response.body.success).toBe(false); expect(response.body.code).toBe('SHARE_ALREADY_EXISTS'); }); it('should reject request without service token', () => { return request(app.getHttpServer()) .post('/backup-share/store') .send(createStoreSharePayload()) .expect(401); }); it('should reject request with invalid service token', () => { return request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', 'invalid-token') .send(createStoreSharePayload()) .expect(401); }); it('should reject request from unauthorized service', () => { const unauthorizedToken = generateServiceToken('unknown-service'); return request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', unauthorizedToken) .send(createStoreSharePayload()) .expect(401); }); it('should validate required fields', async () => { const response = await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send({}) .expect(400); expect(response.body.success).toBe(false); }); it('should validate public key length', async () => { const response = await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send({ ...createStoreSharePayload(), publicKey: 'short', }) .expect(400); expect(response.body.success).toBe(false); }); }); describe('POST /backup-share/retrieve', () => { const storePayload = { userId: '20001', accountSequence: 20001, publicKey: generatePublicKey('c'), encryptedShareData: 'test-encrypted-data', }; beforeEach(async () => { // Store a share first await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send(storePayload); }); it('should retrieve backup share successfully', async () => { const response = await request(app.getHttpServer()) .post('/backup-share/retrieve') .set('X-Service-Token', serviceToken) .send(createRetrieveSharePayload({ userId: storePayload.userId, publicKey: storePayload.publicKey, })) .expect(200); expect(response.body.success).toBe(true); expect(response.body.encryptedShareData).toBeDefined(); expect(response.body.partyIndex).toBe(2); expect(response.body.publicKey).toBe(storePayload.publicKey); // Verify access log created for RETRIEVE action const accessLogs = await prisma.shareAccessLog.findMany({ where: { userId: BigInt(storePayload.userId), action: 'RETRIEVE', }, }); expect(accessLogs.length).toBeGreaterThan(0); expect(accessLogs[0].action).toBe('RETRIEVE'); }); it('should return 404 for non-existent share', async () => { const response = await request(app.getHttpServer()) .post('/backup-share/retrieve') .set('X-Service-Token', serviceToken) .send(createRetrieveSharePayload({ userId: '99999', publicKey: generatePublicKey('z'), })) .expect(404); expect(response.body.success).toBe(false); expect(response.body.code).toBe('SHARE_NOT_FOUND'); }); it('should increment access count on retrieve', async () => { // First retrieve await request(app.getHttpServer()) .post('/backup-share/retrieve') .set('X-Service-Token', serviceToken) .send(createRetrieveSharePayload({ userId: storePayload.userId, publicKey: storePayload.publicKey, })) .expect(200); // Second retrieve await request(app.getHttpServer()) .post('/backup-share/retrieve') .set('X-Service-Token', serviceToken) .send(createRetrieveSharePayload({ userId: storePayload.userId, publicKey: storePayload.publicKey, })) .expect(200); // Verify access count const share = await prisma.backupShare.findUnique({ where: { userId: BigInt(storePayload.userId) }, }); expect(share?.accessCount).toBe(2); }); }); describe('POST /backup-share/revoke', () => { const storePayload = { userId: '30001', accountSequence: 30001, publicKey: generatePublicKey('e'), encryptedShareData: 'test-data', }; beforeEach(async () => { await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send(storePayload); }); it('should revoke backup share successfully', async () => { const response = await request(app.getHttpServer()) .post('/backup-share/revoke') .set('X-Service-Token', serviceToken) .send(createRevokeSharePayload({ userId: storePayload.userId, publicKey: storePayload.publicKey, reason: 'ROTATION', })) .expect(200); expect(response.body.success).toBe(true); expect(response.body.message).toBe('Backup share revoked successfully'); // Verify share status in database const share = await prisma.backupShare.findUnique({ where: { userId: BigInt(storePayload.userId) }, }); expect(share?.status).toBe('REVOKED'); }); it('should not allow retrieval of revoked share', async () => { // Revoke first await request(app.getHttpServer()) .post('/backup-share/revoke') .set('X-Service-Token', serviceToken) .send(createRevokeSharePayload({ userId: storePayload.userId, publicKey: storePayload.publicKey, reason: 'SECURITY_BREACH', })) .expect(200); // Try to retrieve const response = await request(app.getHttpServer()) .post('/backup-share/retrieve') .set('X-Service-Token', serviceToken) .send(createRetrieveSharePayload({ userId: storePayload.userId, publicKey: storePayload.publicKey, })) .expect(400); expect(response.body.code).toBe('SHARE_NOT_ACTIVE'); }); it('should validate reason field', async () => { const response = await request(app.getHttpServer()) .post('/backup-share/revoke') .set('X-Service-Token', serviceToken) .send({ userId: storePayload.userId, publicKey: storePayload.publicKey, reason: 'INVALID_REASON', }) .expect(400); expect(response.body.success).toBe(false); }); 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 payload = { userId: String(40000 + i), accountSequence: 40000 + i, publicKey: generatePublicKey(String(i)), encryptedShareData: 'test-data', }; // Store share await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send(payload) .expect(201); // Revoke with valid reason const response = await request(app.getHttpServer()) .post('/backup-share/revoke') .set('X-Service-Token', serviceToken) .send({ userId: payload.userId, publicKey: payload.publicKey, reason: validReasons[i], }) .expect(200); expect(response.body.success).toBe(true); } }); }); describe('Complete Workflow Tests', () => { it('should complete full lifecycle: store -> retrieve -> revoke', async () => { const publicKey = generatePublicKey('x'); const storePayload = 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(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: '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); }); 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: '50002', accountSequence: 50002, 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: '50002', publicKey })) .expect(200); expect(response.body.success).toBe(true); }); }); describe('Data Persistence Tests', () => { it('should persist encrypted data correctly', async () => { const encryptedData = 'base64-encoded-encrypted-share-data-12345'; const payload = createStoreSharePayload({ userId: '60001', accountSequence: 60001, publicKey: generatePublicKey('p'), encryptedShareData: encryptedData, }); await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send(payload) .expect(201); // Verify data in database (it should be double-encrypted) const share = await prisma.backupShare.findUnique({ where: { userId: BigInt(payload.userId) }, }); expect(share).not.toBeNull(); // The stored data should be different from input (encrypted) expect(share?.encryptedShareData).not.toBe(encryptedData); // But retrieving should give us back the original const retrieveResponse = await request(app.getHttpServer()) .post('/backup-share/retrieve') .set('X-Service-Token', serviceToken) .send(createRetrieveSharePayload({ userId: payload.userId, publicKey: payload.publicKey, })) .expect(200); expect(retrieveResponse.body.encryptedShareData).toBe(encryptedData); }); it('should create audit logs for all operations', async () => { const publicKey = generatePublicKey('q'); const userId = '60003'; // Store await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send(createStoreSharePayload({ userId, accountSequence: 60003, publicKey, })) .expect(201); // Retrieve await request(app.getHttpServer()) .post('/backup-share/retrieve') .set('X-Service-Token', serviceToken) .send(createRetrieveSharePayload({ userId, publicKey })) .expect(200); // Revoke await request(app.getHttpServer()) .post('/backup-share/revoke') .set('X-Service-Token', serviceToken) .send(createRevokeSharePayload({ userId, publicKey })) .expect(200); // Check audit logs const logs = await prisma.shareAccessLog.findMany({ where: { userId: BigInt(userId) }, orderBy: { createdAt: 'asc' }, }); expect(logs.length).toBe(3); expect(logs[0].action).toBe('STORE'); expect(logs[1].action).toBe('RETRIEVE'); expect(logs[2].action).toBe('REVOKE'); }); }); });