135 lines
5.0 KiB
TypeScript
135 lines
5.0 KiB
TypeScript
import { Test, TestingModule } from '@nestjs/testing';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import { GetBackupShareHandler } from '../../../src/application/queries/get-backup-share/get-backup-share.handler';
|
|
import { GetBackupShareQuery } from '../../../src/application/queries/get-backup-share/get-backup-share.query';
|
|
import { BACKUP_SHARE_REPOSITORY, BackupShare, BackupShareStatus } from '../../../src/domain';
|
|
import { AesEncryptionService } from '../../../src/infrastructure/crypto/aes-encryption.service';
|
|
import { AuditLogRepository } from '../../../src/infrastructure/persistence/repositories/audit-log.repository';
|
|
import { ApplicationError } from '../../../src/application/errors/application.error';
|
|
|
|
describe('GetBackupShareHandler', () => {
|
|
let handler: GetBackupShareHandler;
|
|
let mockRepository: any;
|
|
let mockEncryptionService: any;
|
|
let mockAuditLogRepository: any;
|
|
|
|
const createMockShare = (status: BackupShareStatus = BackupShareStatus.ACTIVE) => {
|
|
const share = BackupShare.create({
|
|
userId: BigInt(12345),
|
|
accountSequence: BigInt(1001),
|
|
publicKey: '02' + 'a'.repeat(64),
|
|
encryptedShareData: 'double-encrypted-data',
|
|
encryptionKeyId: 'key-v1',
|
|
});
|
|
share.setShareId(BigInt(1));
|
|
if (status === BackupShareStatus.REVOKED) {
|
|
share.revoke('TEST');
|
|
}
|
|
return share;
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
mockRepository = {
|
|
findByUserIdAndPublicKey: jest.fn(),
|
|
save: jest.fn().mockImplementation((share) => Promise.resolve(share)),
|
|
};
|
|
|
|
mockEncryptionService = {
|
|
decrypt: jest.fn().mockResolvedValue('original-encrypted-data'),
|
|
};
|
|
|
|
mockAuditLogRepository = {
|
|
log: jest.fn().mockResolvedValue(undefined),
|
|
countRetrievesByUserToday: jest.fn().mockResolvedValue(0),
|
|
};
|
|
|
|
const module: TestingModule = await Test.createTestingModule({
|
|
providers: [
|
|
GetBackupShareHandler,
|
|
{ provide: BACKUP_SHARE_REPOSITORY, useValue: mockRepository },
|
|
{ provide: AesEncryptionService, useValue: mockEncryptionService },
|
|
{ provide: AuditLogRepository, useValue: mockAuditLogRepository },
|
|
{
|
|
provide: ConfigService,
|
|
useValue: {
|
|
get: (key: string) => {
|
|
if (key === 'MAX_RETRIEVE_PER_DAY') return 3;
|
|
return undefined;
|
|
},
|
|
},
|
|
},
|
|
],
|
|
}).compile();
|
|
|
|
handler = module.get<GetBackupShareHandler>(GetBackupShareHandler);
|
|
});
|
|
|
|
it('should be defined', () => {
|
|
expect(handler).toBeDefined();
|
|
});
|
|
|
|
describe('execute', () => {
|
|
const validQuery = new GetBackupShareQuery(
|
|
'12345',
|
|
'02' + 'a'.repeat(64),
|
|
'valid-recovery-token',
|
|
'identity-service',
|
|
'127.0.0.1',
|
|
);
|
|
|
|
it('should retrieve backup share successfully', async () => {
|
|
const mockShare = createMockShare();
|
|
mockRepository.findByUserIdAndPublicKey.mockResolvedValue(mockShare);
|
|
|
|
const result = await handler.execute(validQuery);
|
|
|
|
expect(result.encryptedShareData).toBe('original-encrypted-data');
|
|
expect(result.partyIndex).toBe(2);
|
|
expect(result.publicKey).toBe(validQuery.publicKey);
|
|
expect(mockEncryptionService.decrypt).toHaveBeenCalledWith(
|
|
'double-encrypted-data',
|
|
'key-v1',
|
|
);
|
|
expect(mockAuditLogRepository.log).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
action: 'RETRIEVE',
|
|
success: true,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should throw error if share not found', async () => {
|
|
mockRepository.findByUserIdAndPublicKey.mockResolvedValue(null);
|
|
|
|
await expect(handler.execute(validQuery)).rejects.toThrow(ApplicationError);
|
|
await expect(handler.execute(validQuery)).rejects.toThrow('Backup share not found');
|
|
});
|
|
|
|
it('should throw error if share is not active', async () => {
|
|
const revokedShare = createMockShare(BackupShareStatus.REVOKED);
|
|
mockRepository.findByUserIdAndPublicKey.mockResolvedValue(revokedShare);
|
|
|
|
await expect(handler.execute(validQuery)).rejects.toThrow(ApplicationError);
|
|
await expect(handler.execute(validQuery)).rejects.toThrow('Backup share is not active');
|
|
});
|
|
|
|
it('should throw error if rate limit exceeded', async () => {
|
|
mockAuditLogRepository.countRetrievesByUserToday.mockResolvedValue(3);
|
|
|
|
await expect(handler.execute(validQuery)).rejects.toThrow(ApplicationError);
|
|
await expect(handler.execute(validQuery)).rejects.toThrow('Rate limit exceeded');
|
|
});
|
|
|
|
it('should increment access count', async () => {
|
|
const mockShare = createMockShare();
|
|
const initialAccessCount = mockShare.accessCount;
|
|
mockRepository.findByUserIdAndPublicKey.mockResolvedValue(mockShare);
|
|
|
|
await handler.execute(validQuery);
|
|
|
|
expect(mockShare.accessCount).toBe(initialAccessCount + 1);
|
|
expect(mockRepository.save).toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|