/**
* postgres-mcp - pgcrypto Extension Tools Unit Tests
*
* Tests for cryptographic function tools.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import type { PostgresAdapter } from "../../PostgresAdapter.js";
import {
createMockPostgresAdapter,
createMockRequestContext,
} from "../../../../__tests__/mocks/index.js";
import { getPgcryptoTools } from "../pgcrypto.js";
describe("Pgcrypto Tools", () => {
let mockAdapter: ReturnType<typeof createMockPostgresAdapter>;
let mockContext: ReturnType<typeof createMockRequestContext>;
let tools: ReturnType<typeof getPgcryptoTools>;
beforeEach(() => {
vi.clearAllMocks();
mockAdapter = createMockPostgresAdapter();
mockContext = createMockRequestContext();
tools = getPgcryptoTools(mockAdapter as unknown as PostgresAdapter);
});
const findTool = (name: string) => tools.find((t) => t.name === name);
describe("pg_pgcrypto_create_extension", () => {
it("should create pgcrypto extension", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({ rows: [] });
const tool = findTool("pg_pgcrypto_create_extension");
const result = (await tool!.handler({}, mockContext)) as {
success: boolean;
message: string;
};
expect(result.success).toBe(true);
expect(result.message).toContain("pgcrypto");
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("CREATE EXTENSION IF NOT EXISTS pgcrypto"),
);
});
});
describe("pg_pgcrypto_hash", () => {
it("should hash data with SHA-256", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [
{
hash: "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e",
},
],
});
const tool = findTool("pg_pgcrypto_hash");
const result = (await tool!.handler(
{
data: "Hello World",
algorithm: "sha256",
},
mockContext,
)) as { success: boolean; algorithm: string; hash: string };
expect(result.success).toBe(true);
expect(result.algorithm).toBe("sha256");
expect(result.hash).toBeDefined();
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("digest($1, $2)"),
["Hello World", "sha256"],
);
});
it("should use base64 encoding when specified", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ hash: "base64encodedstring" }],
});
const tool = findTool("pg_pgcrypto_hash");
const result = (await tool!.handler(
{
data: "test",
algorithm: "md5",
encoding: "base64",
},
mockContext,
)) as { encoding: string };
expect(result.encoding).toBe("base64");
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("'base64'"),
expect.anything(),
);
});
it("should default to hex encoding", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ hash: "abcdef" }],
});
const tool = findTool("pg_pgcrypto_hash");
const result = (await tool!.handler(
{
data: "test",
algorithm: "sha512",
},
mockContext,
)) as { encoding: string };
expect(result.encoding).toBe("hex");
});
});
describe("pg_pgcrypto_hmac", () => {
it("should compute HMAC with secret key", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ hmac: "hmac_result_here" }],
});
const tool = findTool("pg_pgcrypto_hmac");
const result = (await tool!.handler(
{
data: "message",
key: "secret",
algorithm: "sha256",
},
mockContext,
)) as { success: boolean; hmac: string };
expect(result.success).toBe(true);
expect(result.hmac).toBeDefined();
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("hmac($1, $2, $3)"),
["message", "secret", "sha256"],
);
});
it("should support base64 encoding", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ hmac: "base64hmac" }],
});
const tool = findTool("pg_pgcrypto_hmac");
await tool!.handler(
{
data: "msg",
key: "key",
algorithm: "sha256",
encoding: "base64",
},
mockContext,
);
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("encode(hmac($1, $2, $3), 'base64')"),
expect.anything(),
);
});
});
describe("pg_pgcrypto_encrypt", () => {
it("should encrypt data with password", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ encrypted: "encrypted_base64_data" }],
});
const tool = findTool("pg_pgcrypto_encrypt");
const result = (await tool!.handler(
{
data: "secret message",
password: "mypassword",
},
mockContext,
)) as { success: boolean; encrypted: string; encoding: string };
expect(result.success).toBe(true);
expect(result.encrypted).toBeDefined();
expect(result.encoding).toBe("base64");
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("pgp_sym_encrypt"),
["secret message", "mypassword"],
);
});
it("should support encryption options", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ encrypted: "encrypted_data" }],
});
const tool = findTool("pg_pgcrypto_encrypt");
await tool!.handler(
{
data: "message",
password: "pass",
options: "compress-algo=1",
},
mockContext,
);
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("pgp_sym_encrypt($1, $2, $3)"),
["message", "pass", "compress-algo=1"],
);
});
});
describe("pg_pgcrypto_decrypt", () => {
it("should decrypt data with correct password", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ decrypted: "original message" }],
});
const tool = findTool("pg_pgcrypto_decrypt");
const result = (await tool!.handler(
{
encryptedData: "encrypted_base64",
password: "mypassword",
},
mockContext,
)) as { success: boolean; decrypted: string };
expect(result.success).toBe(true);
expect(result.decrypted).toBe("original message");
});
it("should handle decryption failure", async () => {
mockAdapter.executeQuery.mockRejectedValueOnce(new Error("Wrong key"));
const tool = findTool("pg_pgcrypto_decrypt");
await expect(
tool!.handler(
{
encryptedData: "invalid_data",
password: "wrong_password",
},
mockContext,
),
).rejects.toThrow();
});
});
describe("pg_pgcrypto_gen_random_uuid", () => {
it("should generate a single UUID by default", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ uuid: "550e8400-e29b-41d4-a716-446655440000" }],
});
const tool = findTool("pg_pgcrypto_gen_random_uuid");
const result = (await tool!.handler({}, mockContext)) as {
success: boolean;
uuids: string[];
count: number;
};
expect(result.success).toBe(true);
expect(result.uuids).toHaveLength(1);
expect(result.count).toBe(1);
});
it("should handle undefined params (zero-argument call)", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ uuid: "550e8400-e29b-41d4-a716-446655440000" }],
});
const tool = findTool("pg_pgcrypto_gen_random_uuid");
// Pass undefined to simulate calling without arguments
const result = (await tool!.handler(undefined, mockContext)) as {
success: boolean;
uuids: string[];
count: number;
};
expect(result.success).toBe(true);
expect(result.uuids).toHaveLength(1);
expect(result.count).toBe(1);
});
it("should generate multiple UUIDs when count specified", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ uuid: "uuid-1" }, { uuid: "uuid-2" }, { uuid: "uuid-3" }],
});
const tool = findTool("pg_pgcrypto_gen_random_uuid");
const result = (await tool!.handler({ count: 3 }, mockContext)) as {
uuids: string[];
count: number;
};
expect(result.uuids).toHaveLength(3);
expect(result.count).toBe(3);
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("generate_series(1, $1)"),
[3],
);
});
});
describe("pg_pgcrypto_gen_random_bytes", () => {
it("should generate random bytes in hex", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ random_bytes: "a1b2c3d4e5f6" }],
});
const tool = findTool("pg_pgcrypto_gen_random_bytes");
const result = (await tool!.handler(
{
length: 16,
},
mockContext,
)) as { success: boolean; randomBytes: string; encoding: string };
expect(result.success).toBe(true);
expect(result.encoding).toBe("hex");
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("gen_random_bytes"),
[16, "hex"],
);
});
it("should support base64 encoding", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ random_bytes: "base64random==" }],
});
const tool = findTool("pg_pgcrypto_gen_random_bytes");
const result = (await tool!.handler(
{
length: 32,
encoding: "base64",
},
mockContext,
)) as { encoding: string };
expect(result.encoding).toBe("base64");
});
});
describe("pg_pgcrypto_gen_salt", () => {
it("should generate salt with default algorithm", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ salt: "$2a$06$abcdefghij" }],
});
const tool = findTool("pg_pgcrypto_gen_salt");
const result = (await tool!.handler(
{
type: "bf",
},
mockContext,
)) as { success: boolean; salt: string; type: string };
expect(result.success).toBe(true);
expect(result.salt).toBeDefined();
expect(result.type).toBe("bf");
});
it("should support iterations for blowfish", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ salt: "$2a$12$somesaltvalue" }],
});
const tool = findTool("pg_pgcrypto_gen_salt");
await tool!.handler(
{
type: "bf",
iterations: 12,
},
mockContext,
);
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("gen_salt($1, $2)"),
["bf", 12],
);
});
it("should not pass iterations for md5", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ salt: "$1$abcdefgh" }],
});
const tool = findTool("pg_pgcrypto_gen_salt");
await tool!.handler(
{
type: "md5",
iterations: 10, // Should be ignored
},
mockContext,
);
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("gen_salt($1)"),
["md5"],
);
});
});
describe("pg_pgcrypto_crypt", () => {
it("should hash password with bcrypt salt", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ hash: "$2a$06$hashedpassword" }],
});
const tool = findTool("pg_pgcrypto_crypt");
const result = (await tool!.handler(
{
password: "mypassword",
salt: "$2a$06$somesaltvalue",
},
mockContext,
)) as { success: boolean; hash: string; algorithm: string };
expect(result.success).toBe(true);
expect(result.algorithm).toBe("bcrypt");
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("crypt($1, $2)"),
["mypassword", "$2a$06$somesaltvalue"],
);
});
it("should detect md5 algorithm from salt", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ hash: "$1$salt$hash" }],
});
const tool = findTool("pg_pgcrypto_crypt");
const result = (await tool!.handler(
{
password: "pass",
salt: "$1$saltval$",
},
mockContext,
)) as { algorithm: string };
expect(result.algorithm).toBe("md5");
});
it("should detect xdes algorithm from salt", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ hash: "_hashvalue" }],
});
const tool = findTool("pg_pgcrypto_crypt");
const result = (await tool!.handler(
{
password: "pass",
salt: "_saltvalu",
},
mockContext,
)) as { algorithm: string };
expect(result.algorithm).toBe("xdes");
});
it("should detect des algorithm for other salts", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ hash: "hashvalue" }],
});
const tool = findTool("pg_pgcrypto_crypt");
const result = (await tool!.handler(
{
password: "pass",
salt: "ab",
},
mockContext,
)) as { algorithm: string };
expect(result.algorithm).toBe("des");
});
});
it("should export all 9 pgcrypto tools", () => {
expect(tools).toHaveLength(9);
const toolNames = tools.map((t) => t.name);
expect(toolNames).toContain("pg_pgcrypto_create_extension");
expect(toolNames).toContain("pg_pgcrypto_hash");
expect(toolNames).toContain("pg_pgcrypto_hmac");
expect(toolNames).toContain("pg_pgcrypto_encrypt");
expect(toolNames).toContain("pg_pgcrypto_decrypt");
expect(toolNames).toContain("pg_pgcrypto_gen_random_uuid");
expect(toolNames).toContain("pg_pgcrypto_gen_random_bytes");
expect(toolNames).toContain("pg_pgcrypto_gen_salt");
expect(toolNames).toContain("pg_pgcrypto_crypt");
});
});