Files
wehub-resource-sync 70bf21e064
Deploy (to testing) and Test Playground Preview Worker / Deploy Playground Preview Worker (testing) (push) Has been skipped
Deploy Workers Shared Staging / Deploy Workers Shared Staging (push) Failing after 0s
Prerelease / build (push) Has been skipped
Handle Changesets / Handle Changesets (push) Has been cancelled
Semgrep OSS scan / semgrep-oss (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:11 +08:00

179 lines
6.0 KiB
TypeScript

// Integration tests for the keyring `KeyProvider`s that actually shell
// out to the real OS keychain on the test runner.
//
// The mocked unit tests (`mac-security.test.ts`,
// `linux-secret-tool.test.ts`) cover argv shape, exit-code mapping,
// stdin handling, etc. — they swap the `spawnSync` runner with a fake
// so the developer's real keychain is never touched. That gives fast,
// deterministic coverage of the shape of each subprocess call, but it
// does *not* catch:
// - the actual `security` / `secret-tool` binary changing its
// argument format on a new OS release
// - keychain item-storage round-trip bugs (e.g. encoding issues in
// {@link encodeKeyEnvelope} that only manifest when the OS treats
// the value as opaque bytes vs. UTF-8)
// - permission / TCC prompts that the unit tests can't simulate
//
// This file fills that gap by driving the real CLI on the real OS.
// Each platform is gated behind a `describe.skipIf(...)` so the suite
// gracefully no-ops on incompatible runners (Linux CI containers
// without a D-Bus session, Windows runners without the lazy-installed
// `@napi-rs/keyring` binding, etc.).
//
// Hygiene rules:
// - Use a *unique* service name per test run so we never collide with
// a real wrangler-managed keychain entry on a developer machine.
// - Always `afterEach` cleanup — leaving stray keychain entries on a
// developer's machine is a paper-cut.
// - Validate round-trip via `getKey() === setKey(...)` so encoding
// bugs surface immediately.
import { spawnSync } from "node:child_process";
import { randomBytes } from "node:crypto";
import { afterEach, describe, it } from "vitest";
import {
LinuxSecretToolKeyProvider,
probeSecretTool,
} from "../../../src/credential-store/key-providers/linux-secret-tool";
import { MacSecurityKeyProvider } from "../../../src/credential-store/key-providers/mac-security";
import type { KeyProvider } from "../../../src/credential-store/key-providers/interface";
/**
* Build a fresh unique service name so concurrent test runs (and any
* already-present wrangler entry on the developer's keychain) never
* collide.
*/
function uniqueServiceName(prefix: string): string {
return `${prefix}-${randomBytes(8).toString("hex")}`;
}
/** A round-trip / delete sequence that every backend should pass. */
function runKeyProviderRoundTripContract(
makeProvider: () => KeyProvider,
provider: { current: KeyProvider | undefined }
) {
it("round-trips a 32-byte key through the real OS keychain", ({ expect }) => {
provider.current = makeProvider();
const written = new Uint8Array(randomBytes(32));
expect(provider.current.getKey()).toBeUndefined();
provider.current.setKey(written);
const readBack = provider.current.getKey();
expect(readBack).toBeDefined();
expect(Array.from(readBack as Uint8Array)).toEqual(Array.from(written));
});
it("setKey is idempotent — second write overwrites the first", ({
expect,
}) => {
provider.current = makeProvider();
const first = new Uint8Array(randomBytes(32));
const second = new Uint8Array(randomBytes(32));
provider.current.setKey(first);
provider.current.setKey(second);
const readBack = provider.current.getKey();
expect(Array.from(readBack as Uint8Array)).toEqual(Array.from(second));
});
it("deleteKey clears the entry and is idempotent on the next call", ({
expect,
}) => {
provider.current = makeProvider();
const written = new Uint8Array(randomBytes(32));
provider.current.setKey(written);
expect(provider.current.getKey()).toBeDefined();
provider.current.deleteKey();
expect(provider.current.getKey()).toBeUndefined();
// Second delete must not throw — `KeyProvider.deleteKey()` is
// documented as idempotent.
expect(() => (provider.current as KeyProvider).deleteKey()).not.toThrow();
});
}
/**
* Probe whether the runner machine can actually exercise the
* `secret-tool` backend. Requires both the binary AND a D-Bus session
* (which standard Linux CI containers don't have unless explicitly
* provisioned). `secret-tool store --help` exits cleanly even without
* D-Bus, so we go one step further and try a no-op lookup against a
* service name that's guaranteed not to exist — exit 1 (no match) is
* the success signal; any other exit (e.g. 256 / no D-Bus) means the
* backend isn't usable and the suite should skip.
*/
function isSecretToolBackendReachable(): boolean {
if (process.platform !== "linux") {
return false;
}
if (!probeSecretTool()) {
return false;
}
const r = spawnSync(
"secret-tool",
[
"lookup",
"service",
"@@workers-auth-probe-does-not-exist@@",
"account",
"@@probe@@",
],
{ encoding: "utf-8" }
);
// Exit 1 = no match (D-Bus + keyring both fine).
// Anything else (e.g. "Cannot autolaunch D-Bus without X11 $DISPLAY")
// = unusable; skip.
return r.status === 1;
}
describe.skipIf(process.platform !== "darwin")(
"MacSecurityKeyProvider — integration against /usr/bin/security",
() => {
const provider: { current: KeyProvider | undefined } = {
current: undefined,
};
afterEach(() => {
// Best-effort cleanup so we never leave a stray entry on the
// developer's keychain after a failing test run.
try {
provider.current?.deleteKey();
} catch {
/* swallow — the test under test may have already cleared it */
}
provider.current = undefined;
});
const makeProvider = () =>
new MacSecurityKeyProvider(uniqueServiceName("workers-auth-itest"));
runKeyProviderRoundTripContract(makeProvider, provider);
}
);
describe.skipIf(!isSecretToolBackendReachable())(
"LinuxSecretToolKeyProvider — integration against `secret-tool`",
() => {
const provider: { current: KeyProvider | undefined } = {
current: undefined,
};
afterEach(() => {
try {
provider.current?.deleteKey();
} catch {
/* swallow — see mac integration cleanup note */
}
provider.current = undefined;
});
const makeProvider = () =>
new LinuxSecretToolKeyProvider(uniqueServiceName("workers-auth-itest"));
runKeyProviderRoundTripContract(makeProvider, provider);
}
);