test: add unit tests for credential-encryption.service (API-AI-066)
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 45s

- 17 tests covering encrypt/decrypt/hash/mask/redact + config error
- encrypt↔decrypt round-trip, random IV, base64, empty, unicode
- hash: deterministic, different inputs, empty string
- mask: long keys (4+****+4), short keys (2+****), edge cases
- redact: sk- token replacement, no-op, short sk- skip, empty
- Missing CREDENTIAL_ENCRYPTION_KEY throws on encrypt/decrypt

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-06-18 12:16:53 +08:00
parent 4713758344
commit 5fbd437232

View File

@ -0,0 +1,141 @@
import { CredentialEncryptionService } from './credential-encryption.service';
describe('CredentialEncryptionService', () => {
let service: CredentialEncryptionService;
let mockConfig: any;
beforeEach(() => {
mockConfig = {
get: jest.fn().mockReturnValue('test-encryption-key-32bytes!!'),
};
service = new CredentialEncryptionService(mockConfig as any);
});
// ═══════════════════════════════════════════════════════════════════
// encrypt / decrypt round-trip
// ═══════════════════════════════════════════════════════════════════
describe('encrypt ↔ decrypt', () => {
it('round-trip: encrypt then decrypt returns original plaintext', () => {
const plaintext = 'sk-test-api-key-12345678';
const encrypted = service.encrypt(plaintext);
const decrypted = service.decrypt(encrypted);
expect(decrypted).toBe(plaintext);
});
it('produces different ciphertext for same plaintext (random IV)', () => {
const plaintext = 'sk-mytest';
const c1 = service.encrypt(plaintext);
const c2 = service.encrypt(plaintext);
expect(c1).not.toBe(c2);
});
it('encrypted output is base64', () => {
const c = service.encrypt('hello');
expect(() => Buffer.from(c, 'base64')).not.toThrow();
});
it('handles empty string', () => {
const c = service.encrypt('');
const d = service.decrypt(c);
expect(d).toBe('');
});
it('handles unicode characters', () => {
const plaintext = '密钥测试-key-中文';
const c = service.encrypt(plaintext);
const d = service.decrypt(c);
expect(d).toBe(plaintext);
});
});
// ═══════════════════════════════════════════════════════════════════
// hash
// ═══════════════════════════════════════════════════════════════════
describe('hash', () => {
it('returns deterministic hex hash for same input', () => {
const h1 = service.hash('sk-abc');
const h2 = service.hash('sk-abc');
expect(h1).toBe(h2);
expect(h1).toHaveLength(64); // SHA-256 = 64 hex chars
});
it('produces different hashes for different inputs', () => {
const h1 = service.hash('sk-abc');
const h2 = service.hash('sk-def');
expect(h1).not.toBe(h2);
});
it('handles empty string', () => {
const h = service.hash('');
expect(h).toHaveLength(64);
});
});
// ═══════════════════════════════════════════════════════════════════
// mask
// ═══════════════════════════════════════════════════════════════════
describe('mask', () => {
it('masks long keys: first-4 + **** + last-4', () => {
const masked = service.mask('sk-1234567890abcdef');
expect(masked).toBe('sk-1****cdef');
});
it('masks short keys: first-2 + ****', () => {
const masked = service.mask('sk-abcd');
expect(masked).toBe('sk****');
});
it('handles exactly 8 chars as short path', () => {
const masked = service.mask('12345678');
expect(masked).toBe('12****');
});
it('handles 9 chars as long path', () => {
const masked = service.mask('123456789');
expect(masked).toBe('1234****6789');
});
});
// ═══════════════════════════════════════════════════════════════════
// redact
// ═══════════════════════════════════════════════════════════════════
describe('redact', () => {
it('replaces sk- prefixed tokens in log messages', () => {
const msg = 'Calling API with key sk-1234567890abc and secret sk-abcdefghijklmno';
const redacted = service.redact(msg);
expect(redacted).not.toContain('sk-1234567890abc');
expect(redacted).not.toContain('sk-abcdefghijklmno');
expect(redacted).toContain('***REDACTED***');
});
it('does not modify messages without API keys', () => {
const msg = 'User u1 created job j1 successfully';
expect(service.redact(msg)).toBe(msg);
});
it('does not redact short sk- strings (< 10 chars after prefix)', () => {
const msg = 'sk-short is not a key';
expect(service.redact(msg)).toBe(msg);
});
it('handles empty string', () => {
expect(service.redact('')).toBe('');
});
});
// ═══════════════════════════════════════════════════════════════════
// getEncryptionKey error
// ═══════════════════════════════════════════════════════════════════
describe('missing config', () => {
it('throws when CREDENTIAL_ENCRYPTION_KEY is not configured', () => {
mockConfig.get.mockReturnValue(null);
expect(() => service.encrypt('foo')).toThrow('CREDENTIAL_ENCRYPTION_KEY not configured');
expect(() => service.decrypt('foo')).toThrow('CREDENTIAL_ENCRYPTION_KEY not configured');
});
});
});