rwadurian/backend/services/backup-service/test/e2e/backup-share-mock.e2e-spec.ts

434 lines
15 KiB
TypeScript

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');
});
});
});