singularity-forge/packages/mcp-server/src/secure-env-collect.test.ts
2026-05-05 14:46:18 +02:00

227 lines
7.6 KiB
TypeScript

// @singularity-forge/mcp-server — Tests for secure_env_collect MCP tool
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
//
// Tests the secure_env_collect tool registered in createMcpServer.
import assert from "node:assert/strict";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, it } from "vitest";
import { createMcpServer } from "./server.js";
import { SessionManager } from "./session-manager.js";
// ---------------------------------------------------------------------------
// Test helpers
// ---------------------------------------------------------------------------
/**
* Since createMcpServer uses dynamic import for McpServer, we can't easily
* mock it. Instead, we test the env-writer utilities directly (in env-writer.test.ts)
* and test the tool integration by verifying:
* 1. The tool exists in the registered tools list
* 2. The handler produces correct results with mock data
*
* For handler-level testing, we create a standalone test that replicates
* the tool handler logic with a controllable mock.
*/
function makeTempDir(prefix: string): string {
return mkdtempSync(join(tmpdir(), `${prefix}-`));
}
// ---------------------------------------------------------------------------
// Integration test — verify tool is registered
// ---------------------------------------------------------------------------
describe("secure_env_collect tool registration", () => {
it("createMcpServer registers secure_env_collect tool", async () => {
// This test verifies the tool exists — createMcpServer internally calls
// server.tool('secure_env_collect', ...) which we can't intercept without
// module mocking, but we can verify the server creates successfully
const sm = new SessionManager();
try {
const { server } = await createMcpServer(sm);
assert.ok(server, "server should be created");
// The McpServer internally tracks registered tools — we verify no error
} finally {
await sm.cleanup();
}
});
});
// ---------------------------------------------------------------------------
// Handler logic tests — using env-writer directly to test the flow
// ---------------------------------------------------------------------------
describe("secure_env_collect handler logic", () => {
it("skips keys that already exist in .env", async () => {
const tmp = makeTempDir("sec-collect");
try {
const envPath = join(tmp, ".env");
writeFileSync(envPath, "ALREADY_SET=existing-value\n");
// Import the utility directly to test the pre-check logic
const { checkExistingEnvKeys } = await import("./env-writer.js");
const existing = await checkExistingEnvKeys(
["ALREADY_SET", "NEW_KEY"],
envPath,
);
assert.deepStrictEqual(existing, ["ALREADY_SET"]);
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
it("writes collected values to .env without returning secret values", async () => {
const tmp = makeTempDir("sec-collect");
try {
const envPath = join(tmp, ".env");
const savedKey = process.env.SEC_COLLECT_TEST_KEY;
const { applySecrets } = await import("./env-writer.js");
const { applied, errors } = await applySecrets(
[{ key: "SEC_COLLECT_TEST_KEY", value: "super-secret-value" }],
"dotenv",
{ envFilePath: envPath },
);
assert.deepStrictEqual(applied, ["SEC_COLLECT_TEST_KEY"]);
assert.deepStrictEqual(errors, []);
// Verify the value was written
const content = readFileSync(envPath, "utf8");
assert.ok(content.includes("SEC_COLLECT_TEST_KEY=super-secret-value"));
// Verify process.env was hydrated
assert.equal(process.env.SEC_COLLECT_TEST_KEY, "super-secret-value");
// Cleanup
if (savedKey === undefined) delete process.env.SEC_COLLECT_TEST_KEY;
else process.env.SEC_COLLECT_TEST_KEY = savedKey;
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
it("auto-detects vercel destination from vercel.json", async () => {
const tmp = makeTempDir("sec-collect");
try {
writeFileSync(join(tmp, "vercel.json"), "{}");
const { detectDestination } = await import("./env-writer.js");
assert.equal(detectDestination(tmp), "vercel");
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
it("handles empty form values as skipped", async () => {
// Simulate what happens when user leaves a field empty in the form
const formContent: Record<string, string> = {
API_KEY: "provided-value",
OPTIONAL_KEY: "", // empty = skip
};
const provided: Array<{ key: string; value: string }> = [];
const skipped: string[] = [];
for (const [key, raw] of Object.entries(formContent)) {
const value = typeof raw === "string" ? raw.trim() : "";
if (value.length > 0) {
provided.push({ key, value });
} else {
skipped.push(key);
}
}
assert.deepStrictEqual(provided, [
{ key: "API_KEY", value: "provided-value" },
]);
assert.deepStrictEqual(skipped, ["OPTIONAL_KEY"]);
});
it("result text never contains secret values", async () => {
const tmp = makeTempDir("sec-collect");
try {
const envPath = join(tmp, ".env");
const savedKey = process.env.RESULT_TEXT_TEST;
const { applySecrets } = await import("./env-writer.js");
const { applied } = await applySecrets(
[{ key: "RESULT_TEXT_TEST", value: "sk-super-secret-abc123" }],
"dotenv",
{ envFilePath: envPath },
);
// Simulate building result text (same logic as the tool handler)
const lines: string[] = [
"destination: dotenv (auto-detected)",
...applied.map((k) => `${k}: applied`),
];
const resultText = lines.join("\n");
// The result MUST NOT contain the secret value
assert.ok(
!resultText.includes("sk-super-secret-abc123"),
"result text must not contain secret value",
);
assert.ok(
resultText.includes("RESULT_TEXT_TEST"),
"result text should contain key name",
);
// Cleanup
if (savedKey === undefined) delete process.env.RESULT_TEXT_TEST;
else process.env.RESULT_TEXT_TEST = savedKey;
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
it("handles multiple keys with mixed existing/new/skipped", async () => {
const tmp = makeTempDir("sec-collect");
try {
const envPath = join(tmp, ".env");
writeFileSync(envPath, "EXISTING_A=already-here\n");
const savedB = process.env.NEW_B;
const savedC = process.env.SKIP_C;
const { checkExistingEnvKeys, applySecrets } = await import(
"./env-writer.js"
);
const allKeys = ["EXISTING_A", "NEW_B", "SKIP_C"];
const existing = await checkExistingEnvKeys(allKeys, envPath);
assert.deepStrictEqual(existing, ["EXISTING_A"]);
// Simulate form response: NEW_B has value, SKIP_C is empty
const formContent = { NEW_B: "new-value", SKIP_C: "" };
const provided: Array<{ key: string; value: string }> = [];
const skipped: string[] = [];
for (const key of allKeys.filter((k) => !existing.includes(k))) {
const raw = formContent[key as keyof typeof formContent] ?? "";
if (raw.trim().length > 0) provided.push({ key, value: raw.trim() });
else skipped.push(key);
}
const { applied, errors } = await applySecrets(provided, "dotenv", {
envFilePath: envPath,
});
assert.deepStrictEqual(applied, ["NEW_B"]);
assert.deepStrictEqual(skipped, ["SKIP_C"]);
assert.deepStrictEqual(errors, []);
assert.deepStrictEqual(existing, ["EXISTING_A"]);
// Cleanup
if (savedB === undefined) delete process.env.NEW_B;
else process.env.NEW_B = savedB;
if (savedC === undefined) delete process.env.SKIP_C;
else process.env.SKIP_C = savedC;
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
});