85 lines
4.2 KiB
TypeScript
85 lines
4.2 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { mkdtemp, writeFile, rm, mkdir } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import test from "node:test";
|
|
import { createCredentialResolver } from "./credentials.ts";
|
|
import type { CredentialBindings } from "./contracts.ts";
|
|
|
|
const signal = () => new AbortController().signal;
|
|
async function fixture(fn: (cwd: string) => Promise<void>) {
|
|
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-credentials-"));
|
|
try { await fn(cwd); } finally { await rm(cwd, { recursive: true, force: true }); }
|
|
}
|
|
|
|
test("literals and explicit environment sources resolve without trimming or process-env fallback", async () => {
|
|
const resolve = createCredentialResolver({ literal: " test-value\n", environment: { env: "TEST_PASSWORD" }, blank: "" }, {
|
|
cwd: "/unused", env: { TEST_PASSWORD: "another-value" },
|
|
});
|
|
assert.equal(await resolve("literal", signal()), " test-value\n");
|
|
assert.equal(await resolve("environment", signal()), "another-value");
|
|
assert.equal(await resolve("blank", signal()), "");
|
|
await assert.rejects(resolve("missing", signal()), /not supplied/);
|
|
});
|
|
|
|
test("plain file and exact JSON key sources resolve relative to the caller cwd", async () => {
|
|
await fixture(async (cwd) => {
|
|
await writeFile(join(cwd, "password.txt"), "plain-secret\n", { mode: 0o600 });
|
|
await writeFile(join(cwd, "accounts.json"), JSON.stringify({ "test.password": "json-secret", user: "demo" }), { mode: 0o600 });
|
|
const resolve = createCredentialResolver({
|
|
plain: { file: "password.txt" },
|
|
json: { file: "accounts.json", key: "test.password" },
|
|
}, { cwd, env: {} });
|
|
assert.equal(await resolve("plain", signal()), "plain-secret\n");
|
|
assert.equal(await resolve("json", signal()), "json-secret");
|
|
});
|
|
});
|
|
|
|
test("files are read lazily and malformed/missing values never appear in errors", async () => {
|
|
await fixture(async (cwd) => {
|
|
const resolve = createCredentialResolver({ secret: { file: "accounts.json", key: "password" } }, { cwd, env: {} });
|
|
await writeFile(join(cwd, "accounts.json"), "private-secret-is-not-json");
|
|
await assert.rejects(resolve("secret", signal()), (error: Error) => {
|
|
assert.match(error.message, /not valid JSON/);
|
|
assert.doesNotMatch(error.message, /private-secret/);
|
|
return true;
|
|
});
|
|
await writeFile(join(cwd, "accounts.json"), JSON.stringify({ password: "new-value" }));
|
|
assert.equal(await resolve("secret", signal()), "new-value");
|
|
await writeFile(join(cwd, "accounts.json"), JSON.stringify({ elsewhere: "private-value" }));
|
|
await assert.rejects(resolve("secret", signal()), /entry is missing/);
|
|
});
|
|
});
|
|
|
|
test("missing environment and oversized values fail closed", async () => {
|
|
const resolve = createCredentialResolver({ p: { env: "PRIVATE_VALUE" } }, { cwd: "/unused", env: {} });
|
|
await assert.rejects(resolve("p", signal()), /bounded string/);
|
|
assert.throws(() => createCredentialResolver({ p: "x".repeat(8001) }, { cwd: "/unused" }), /too long/);
|
|
});
|
|
|
|
test("unknown source fields and ambiguous bindings are rejected before any IO", () => {
|
|
for (const bindings of [
|
|
{ p: { env: "A", file: "x" } }, { p: { command: "print-secret" } }, { p: { env: "A B" } },
|
|
{ p: { file: "x", key: 2 } }, { "bad name": "value" }, { p: null },
|
|
]) {
|
|
assert.throws(() => createCredentialResolver(bindings as unknown as CredentialBindings, { cwd: "/unused" }));
|
|
}
|
|
});
|
|
|
|
test("non-regular and oversized files are rejected", async () => {
|
|
await fixture(async (cwd) => {
|
|
await mkdir(join(cwd, "directory"));
|
|
await writeFile(join(cwd, "large"), "x".repeat(65537));
|
|
for (const file of ["directory", "large"]) {
|
|
const resolve = createCredentialResolver({ p: { file } }, { cwd, env: {} });
|
|
await assert.rejects(resolve("p", signal()), /bounded regular file/);
|
|
}
|
|
});
|
|
});
|
|
|
|
test("pre-aborted credential resolution does not read the source", async () => {
|
|
const resolve = createCredentialResolver({ p: { file: "/nonexistent" } }, { cwd: "/unused", env: {} });
|
|
const abort = new AbortController(); abort.abort(new Error("test cancellation"));
|
|
await assert.rejects(resolve("p", abort.signal), /test cancellation/);
|
|
});
|