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
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:
parent
4713758344
commit
5fbd437232
141
src/modules/ai-runtime/credential-encryption.service.spec.ts
Normal file
141
src/modules/ai-runtime/credential-encryption.service.spec.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user