/** * Crypto Service Tests */ import { describe, it, expect, beforeEach, vi } from "vitest"; import { Test, TestingModule } from "@nestjs/testing"; import { ConfigService } from "@nestjs/config"; import { CryptoService } from "./crypto.service"; import { Logger } from "@nestjs/common"; describe("CryptoService", () => { let service: CryptoService; // Valid 32-byte hex key for testing const testEncryptionKey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ CryptoService, { provide: ConfigService, useValue: { get: (key: string) => { if (key === "ENCRYPTION_KEY") return testEncryptionKey; return undefined; }, }, }, ], }).compile(); service = module.get(CryptoService); }); describe("initialization", () => { it("should throw error if ENCRYPTION_KEY is missing", () => { expect(() => { new CryptoService({ get: () => undefined, } as ConfigService); }).toThrow("ENCRYPTION_KEY environment variable is required"); }); it("should throw error if ENCRYPTION_KEY is invalid length", () => { expect(() => { new CryptoService({ get: () => "invalid", } as ConfigService); }).toThrow("ENCRYPTION_KEY must be 64 hexadecimal characters"); }); it("should initialize successfully with valid key", () => { expect(service).toBeDefined(); }); }); describe("encrypt", () => { it("should encrypt plaintext data", () => { // Arrange const plaintext = "sensitive data"; // Act const encrypted = service.encrypt(plaintext); // Assert expect(encrypted).toBeDefined(); expect(encrypted).not.toEqual(plaintext); expect(encrypted.split(":")).toHaveLength(3); // iv:authTag:encrypted }); it("should produce different ciphertext for same plaintext", () => { // Arrange const plaintext = "sensitive data"; // Act const encrypted1 = service.encrypt(plaintext); const encrypted2 = service.encrypt(plaintext); // Assert expect(encrypted1).not.toEqual(encrypted2); // Different IVs }); it("should encrypt long data (RSA private key)", () => { // Arrange const longData = "-----BEGIN PRIVATE KEY-----\n" + "a".repeat(1000) + "\n-----END PRIVATE KEY-----"; // Act const encrypted = service.encrypt(longData); // Assert expect(encrypted).toBeDefined(); expect(encrypted.length).toBeGreaterThan(0); }); }); describe("decrypt", () => { it("should decrypt encrypted data", () => { // Arrange const plaintext = "sensitive data"; const encrypted = service.encrypt(plaintext); // Act const decrypted = service.decrypt(encrypted); // Assert expect(decrypted).toEqual(plaintext); }); it("should decrypt long data", () => { // Arrange const longData = "-----BEGIN PRIVATE KEY-----\n" + "a".repeat(1000) + "\n-----END PRIVATE KEY-----"; const encrypted = service.encrypt(longData); // Act const decrypted = service.decrypt(encrypted); // Assert expect(decrypted).toEqual(longData); }); it("should throw error for invalid encrypted data format", () => { // Arrange const invalidData = "invalid:format"; // Act & Assert expect(() => service.decrypt(invalidData)).toThrow("Failed to decrypt data"); }); it("should throw error for corrupted data", () => { // Arrange const plaintext = "sensitive data"; const encrypted = service.encrypt(plaintext); const corrupted = encrypted.replace(/[0-9a-f]/, "x"); // Corrupt one character // Act & Assert expect(() => service.decrypt(corrupted)).toThrow("Failed to decrypt data"); }); it("should not log sensitive data in error messages", () => { // Arrange const loggerErrorSpy = vi.spyOn(Logger.prototype, "error"); const corruptedData = "corrupted:data:here"; // Act & Assert expect(() => service.decrypt(corruptedData)).toThrow("Failed to decrypt data"); // Verify logger was called with safe message expect(loggerErrorSpy).toHaveBeenCalled(); const logCall = loggerErrorSpy.mock.calls[0]; // First argument should contain error type but not sensitive data expect(logCall[0]).toMatch(/Decryption failed:/); // Should NOT log the actual error object with stack traces expect(logCall.length).toBe(1); // Only one argument (the message) // Verify the corrupted data is not in the log const logMessage = logCall[0] as string; expect(logMessage).not.toContain(corruptedData); loggerErrorSpy.mockRestore(); }); }); describe("encrypt/decrypt round-trip", () => { it("should maintain data integrity through encrypt-decrypt cycle", () => { // Arrange const testCases = [ "short", "medium length string with special chars !@#$%", "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC\n-----END PRIVATE KEY-----", JSON.stringify({ complex: "object", with: ["arrays", 123] }), ]; testCases.forEach((plaintext) => { // Act const encrypted = service.encrypt(plaintext); const decrypted = service.decrypt(encrypted); // Assert expect(decrypted).toEqual(plaintext); }); }); }); });