161 lines
5.4 KiB
TypeScript
161 lines
5.4 KiB
TypeScript
import { describe, test, expect } from "bun:test";
|
|
import { generateSecureValue } from "../../src/modules/credentials/generateSecureValue";
|
|
import { readSecureValue } from "../../src/modules/credentials/readSecureValue";
|
|
|
|
/**
|
|
* Tests for the secure value encryption/decryption round-trip.
|
|
*
|
|
* The test setup.ts mocks the constants module with a matching RSA key pair,
|
|
* so generateSecureValue (uses public key) and readSecureValue (uses private key)
|
|
* will work correctly together.
|
|
*/
|
|
|
|
describe("generateSecureValue", () => {
|
|
test("returns an object with encrypted and hash fields", () => {
|
|
const result = generateSecureValue("my-secret-password");
|
|
expect(result).toHaveProperty("encrypted");
|
|
expect(result).toHaveProperty("hash");
|
|
expect(typeof result.encrypted).toBe("string");
|
|
expect(typeof result.hash).toBe("string");
|
|
});
|
|
|
|
test("encrypted is a valid base64 string", () => {
|
|
const result = generateSecureValue("test-value");
|
|
expect(() => Buffer.from(result.encrypted, "base64")).not.toThrow();
|
|
const decoded = Buffer.from(result.encrypted, "base64");
|
|
expect(decoded.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("hash follows BLAKE2s format", () => {
|
|
const result = generateSecureValue("test-value");
|
|
expect(result.hash).toMatch(/^BLAKE2s\$/);
|
|
const parts = result.hash.split("$");
|
|
expect(parts[0]).toBe("BLAKE2s");
|
|
expect(parts[1].length).toBeGreaterThan(0); // hex hash
|
|
});
|
|
|
|
test("different inputs produce different encrypted values", () => {
|
|
const a = generateSecureValue("password-a");
|
|
const b = generateSecureValue("password-b");
|
|
expect(a.encrypted).not.toBe(b.encrypted);
|
|
});
|
|
|
|
test("different inputs produce different hashes", () => {
|
|
const a = generateSecureValue("password-a");
|
|
const b = generateSecureValue("password-b");
|
|
expect(a.hash).not.toBe(b.hash);
|
|
});
|
|
|
|
test("encrypts empty string without error", () => {
|
|
const result = generateSecureValue("");
|
|
expect(result.encrypted.length).toBeGreaterThan(0);
|
|
expect(result.hash.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("encrypts special characters", () => {
|
|
const result = generateSecureValue('p@$$w0rd!#%^&*(){}[]|\\:";<>?,./~`');
|
|
expect(result.encrypted.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("encrypts Unicode content", () => {
|
|
const result = generateSecureValue("密码测试 🔐");
|
|
expect(result.encrypted.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe("readSecureValue", () => {
|
|
test("decrypts a value encrypted by generateSecureValue", () => {
|
|
const original = "my-secret-password";
|
|
const { encrypted } = generateSecureValue(original);
|
|
const decrypted = readSecureValue(encrypted);
|
|
expect(decrypted).toBe(original);
|
|
});
|
|
|
|
test("decrypts empty string", () => {
|
|
const { encrypted } = generateSecureValue("");
|
|
const decrypted = readSecureValue(encrypted);
|
|
expect(decrypted).toBe("");
|
|
});
|
|
|
|
test("decrypts special characters", () => {
|
|
const original = 'p@$$w0rd!#%^&*(){}[]|\\:";<>?,./~`';
|
|
const { encrypted } = generateSecureValue(original);
|
|
const decrypted = readSecureValue(encrypted);
|
|
expect(decrypted).toBe(original);
|
|
});
|
|
|
|
test("decrypts Unicode content", () => {
|
|
const original = "密码测试 🔐";
|
|
const { encrypted } = generateSecureValue(original);
|
|
const decrypted = readSecureValue(encrypted);
|
|
expect(decrypted).toBe(original);
|
|
});
|
|
|
|
test("validates hash when provided and correct", () => {
|
|
const original = "test-value";
|
|
const { encrypted, hash } = generateSecureValue(original);
|
|
const decrypted = readSecureValue(encrypted, hash);
|
|
expect(decrypted).toBe(original);
|
|
});
|
|
|
|
test("throws when hash validation fails", () => {
|
|
const original = "test-value";
|
|
const { encrypted } = generateSecureValue(original);
|
|
const badHash =
|
|
"BLAKE2s$0000000000000000000000000000000000000000000000000000000000000000$salt";
|
|
expect(() => readSecureValue(encrypted, badHash)).toThrow(
|
|
"Secure value hash validation failed",
|
|
);
|
|
});
|
|
|
|
test("throws GenericError on invalid encrypted content", () => {
|
|
try {
|
|
readSecureValue("not-valid-encrypted-data");
|
|
expect(true).toBe(false); // should not reach
|
|
} catch (e: any) {
|
|
expect(e.name).toBe("SecureValueDecryptionError");
|
|
expect(e.status).toBe(422);
|
|
}
|
|
});
|
|
|
|
test("throws GenericError with descriptive message on key mismatch", () => {
|
|
try {
|
|
readSecureValue("dGhpcyBpcyBub3QgZW5jcnlwdGVk"); // base64 but not RSA
|
|
expect(true).toBe(false);
|
|
} catch (e: any) {
|
|
expect(e.message).toContain("Unable to decrypt secure value");
|
|
expect(e.cause).toContain("RSA key mismatch");
|
|
}
|
|
});
|
|
|
|
test("skips hash validation when hash is not provided", () => {
|
|
const original = "no-hash-check";
|
|
const { encrypted } = generateSecureValue(original);
|
|
// Should not throw even though no hash is passed
|
|
const decrypted = readSecureValue(encrypted);
|
|
expect(decrypted).toBe(original);
|
|
});
|
|
});
|
|
|
|
describe("generateSecureValue + readSecureValue round-trip", () => {
|
|
test("round-trips various string types", () => {
|
|
const testValues = [
|
|
"simple",
|
|
"",
|
|
"a".repeat(100),
|
|
"line1\nline2\ttab",
|
|
'{"json": true}',
|
|
"null",
|
|
"undefined",
|
|
"0",
|
|
" leading-trailing ",
|
|
];
|
|
|
|
for (const original of testValues) {
|
|
const { encrypted, hash } = generateSecureValue(original);
|
|
const decrypted = readSecureValue(encrypted, hash);
|
|
expect(decrypted).toBe(original);
|
|
}
|
|
});
|
|
});
|