import { describe, it, expect, beforeEach } from "vitest"; import { ConfigService } from "@nestjs/config"; import { CryptoService } from "./crypto.service"; function createConfigService(secret?: string): ConfigService { return { get: (key: string) => { if (key === "MOSAIC_SECRET_KEY") { return secret; } return undefined; }, } as unknown as ConfigService; } describe("CryptoService", () => { let service: CryptoService; beforeEach(() => { service = new CryptoService(createConfigService("this-is-a-test-secret-key-with-32+chars")); }); it("encrypt -> decrypt roundtrip", () => { const plaintext = "my-secret-api-key"; const encrypted = service.encrypt(plaintext); const decrypted = service.decrypt(encrypted); expect(encrypted.startsWith("enc:")).toBe(true); expect(decrypted).toBe(plaintext); }); it("decrypt rejects tampered ciphertext", () => { const encrypted = service.encrypt("sensitive-token"); const payload = encrypted.slice(4); const bytes = Buffer.from(payload, "base64"); bytes[bytes.length - 1] = bytes[bytes.length - 1]! ^ 0xff; const tampered = `enc:${bytes.toString("base64")}`; expect(() => service.decrypt(tampered)).toThrow(); }); it("decrypt rejects non-encrypted string", () => { expect(() => service.decrypt("plain-text-value")).toThrow(); }); it("isEncrypted detects prefix correctly", () => { expect(service.isEncrypted("enc:abc")).toBe(true); expect(service.isEncrypted("ENC:abc")).toBe(false); expect(service.isEncrypted("plain-text")).toBe(false); }); it("generateToken returns 64-char hex string", () => { const token = service.generateToken(); expect(token).toMatch(/^[0-9a-f]{64}$/); }); it("different plaintexts produce different ciphertexts (random IV)", () => { const encryptedA = service.encrypt("value-a"); const encryptedB = service.encrypt("value-b"); expect(encryptedA).not.toBe(encryptedB); }); it("missing MOSAIC_SECRET_KEY throws on construction", () => { expect(() => new CryptoService(createConfigService(undefined))).toThrow(); }); });