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