rwadurian/backend/services/backup-service/test/unit/infrastructure/aes-encryption.service.spec.ts

143 lines
4.6 KiB
TypeScript

import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { AesEncryptionService } from '../../../src/infrastructure/crypto/aes-encryption.service';
describe('AesEncryptionService', () => {
let service: AesEncryptionService;
const testKey = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
const testKeyId = 'test-key-v1';
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AesEncryptionService,
{
provide: ConfigService,
useValue: {
get: (key: string) => {
switch (key) {
case 'BACKUP_ENCRYPTION_KEY':
return testKey;
case 'BACKUP_ENCRYPTION_KEY_ID':
return testKeyId;
default:
return undefined;
}
},
},
},
],
}).compile();
service = module.get<AesEncryptionService>(AesEncryptionService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('encrypt', () => {
it('should encrypt plaintext successfully', async () => {
const plaintext = 'Hello, World!';
const result = await service.encrypt(plaintext);
expect(result.encrypted).toBeDefined();
expect(result.keyId).toBe(testKeyId);
expect(result.encrypted).not.toBe(plaintext);
// Should contain 3 parts separated by ':'
expect(result.encrypted.split(':').length).toBe(3);
});
it('should produce different ciphertext for same plaintext (due to random IV)', async () => {
const plaintext = 'Same message';
const result1 = await service.encrypt(plaintext);
const result2 = await service.encrypt(plaintext);
expect(result1.encrypted).not.toBe(result2.encrypted);
});
});
describe('decrypt', () => {
it('should decrypt ciphertext back to original plaintext', async () => {
const plaintext = 'Secret message for testing';
const { encrypted, keyId } = await service.encrypt(plaintext);
const decrypted = await service.decrypt(encrypted, keyId);
expect(decrypted).toBe(plaintext);
});
it('should handle special characters', async () => {
const plaintext = 'Special chars: !@#$%^&*()_+-={}[]|\\:";\'<>?,./';
const { encrypted, keyId } = await service.encrypt(plaintext);
const decrypted = await service.decrypt(encrypted, keyId);
expect(decrypted).toBe(plaintext);
});
it('should handle unicode characters', async () => {
const plaintext = 'Unicode: 你好世界 🌍 مرحبا';
const { encrypted, keyId } = await service.encrypt(plaintext);
const decrypted = await service.decrypt(encrypted, keyId);
expect(decrypted).toBe(plaintext);
});
it('should handle large payloads', async () => {
const plaintext = 'a'.repeat(10000);
const { encrypted, keyId } = await service.encrypt(plaintext);
const decrypted = await service.decrypt(encrypted, keyId);
expect(decrypted).toBe(plaintext);
});
it('should throw error for invalid format', async () => {
await expect(service.decrypt('invalid-format', testKeyId)).rejects.toThrow(
'Invalid encrypted data format',
);
});
it('should throw error for non-existent key', async () => {
const { encrypted } = await service.encrypt('test');
await expect(service.decrypt(encrypted, 'non-existent-key')).rejects.toThrow(
'Decryption key not found',
);
});
});
describe('addKey', () => {
it('should add new key for rotation', async () => {
const newKeyId = 'key-v2';
const newKey = 'fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210';
service.addKey(newKeyId, newKey);
// Encrypt with original key
const { encrypted: encrypted1 } = await service.encrypt('test');
// Should still be able to decrypt (uses current key)
const decrypted = await service.decrypt(encrypted1, testKeyId);
expect(decrypted).toBe('test');
});
it('should throw error for invalid key length', () => {
expect(() => service.addKey('bad-key', 'too-short')).toThrow(
'Key must be 256 bits',
);
});
});
describe('getCurrentKeyId', () => {
it('should return current key ID', () => {
expect(service.getCurrentKeyId()).toBe(testKeyId);
});
});
});