Files
hyperframes/packages/cli/src/auth/resolver.test.ts
T

144 lines
5.2 KiB
TypeScript

import { promises as fs } from "node:fs";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { setupTempAuthEnv } from "./_test-utils.js";
import { isAuthError } from "./errors.js";
import { resolveCredential, tryResolveCredential } from "./resolver.js";
import { writeStore } from "./store.js";
describe("auth/resolver", () => {
let fixture: Awaited<ReturnType<typeof setupTempAuthEnv>>;
let dir: string;
beforeEach(async () => {
fixture = await setupTempAuthEnv("hf-auth-resolve-");
dir = fixture.dir;
});
afterEach(async () => {
await fixture.restore();
});
it("prefers HEYGEN_API_KEY over everything else", async () => {
process.env["HEYGEN_API_KEY"] = "env-key";
process.env["HYPERFRAMES_API_KEY"] = "alias-key";
await writeStore({ api_key: "file-key" });
const r = await resolveCredential();
expect(r).toEqual({ type: "api_key", key: "env-key", source: "env" });
});
it("falls through to HYPERFRAMES_API_KEY", async () => {
process.env["HYPERFRAMES_API_KEY"] = "alias-key";
await writeStore({ api_key: "file-key" });
const r = await resolveCredential();
expect(r).toEqual({ type: "api_key", key: "alias-key", source: "env_alias" });
});
it("returns file api_key when no env is set", async () => {
await writeStore({ api_key: "file-key" });
const r = await resolveCredential();
expect(r.type).toBe("api_key");
if (r.type === "api_key") {
expect(r.key).toBe("file-key");
expect(r.source).toBe("file_json");
}
});
it("prefers fresh oauth over api_key", async () => {
const future = new Date(Date.now() + 60 * 60 * 1000).toISOString();
await writeStore({
api_key: "file-key",
oauth: { access_token: "fresh-at", refresh_token: "rt", expires_at: future },
});
const r = await resolveCredential();
expect(r.type).toBe("oauth");
if (r.type === "oauth") {
expect(r.access_token).toBe("fresh-at");
// Fresh access_token does NOT need refresh, even with refresh_token present.
expect(r.refreshable).toBe(false);
}
});
it("oauth without expires_at is treated as fresh (refreshable=false)", async () => {
await writeStore({
oauth: { access_token: "at", refresh_token: "rt" },
});
const r = await resolveCredential();
expect(r.type).toBe("oauth");
if (r.type === "oauth") expect(r.refreshable).toBe(false);
});
it("marks expired-but-refreshable oauth as refreshable", async () => {
const past = new Date(Date.now() - 60 * 60 * 1000).toISOString();
await writeStore({
oauth: { access_token: "stale-at", refresh_token: "rt", expires_at: past },
});
const r = await resolveCredential();
expect(r.type).toBe("oauth");
if (r.type === "oauth") expect(r.refreshable).toBe(true);
});
it("skips expired non-refreshable oauth and falls through to api_key", async () => {
const past = new Date(Date.now() - 60 * 60 * 1000).toISOString();
await writeStore({
api_key: "fallback",
oauth: { access_token: "stale-at", expires_at: past },
});
const r = await resolveCredential();
expect(r.type).toBe("api_key");
if (r.type === "api_key") expect(r.key).toBe("fallback");
});
it("rejects HEYGEN_API_KEY containing CRLF (header-injection guard)", async () => {
process.env["HEYGEN_API_KEY"] = "hg_x\r\nX-Evil: 1";
await expect(resolveCredential()).rejects.toSatisfy((err) => {
return isAuthError(err) && (err as { code: string }).code === "INVALID_STORE";
});
});
it("throws ErrNotConfigured when nothing is configured", async () => {
await expect(resolveCredential()).rejects.toSatisfy((err) => {
return isAuthError(err) && (err as { code: string }).code === "NOT_CONFIGURED";
});
});
it("identifies legacy plaintext file source", async () => {
const path = join(dir, "credentials");
await fs.writeFile(path, "hg_legacy_key", { mode: 0o600 });
const r = await resolveCredential();
expect(r.type).toBe("api_key");
if (r.type === "api_key") {
expect(r.key).toBe("hg_legacy_key");
expect(r.source).toBe("file_legacy");
}
});
it("tryResolveCredential returns null when not configured", async () => {
expect(await tryResolveCredential()).toBeNull();
});
it("tryResolveCredential surfaces broken-file errors", async () => {
const path = join(dir, "credentials");
await fs.writeFile(path, "{not valid", { mode: 0o600 });
await expect(tryResolveCredential()).rejects.toSatisfy((err) => isAuthError(err));
});
it("uses injected now() for expiry decisions", async () => {
// expires_at is one hour ago in real time. Injecting `now` two
// hours in the past makes the token appear fresh (still valid for
// another hour), so the resolver should NOT mark it refreshable.
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
await writeStore({
oauth: { access_token: "at", refresh_token: "rt", expires_at: oneHourAgo },
});
const r = await resolveCredential({
now: () => new Date(Date.now() - 2 * 60 * 60 * 1000),
});
expect(r.type).toBe("oauth");
if (r.type === "oauth") {
expect(r.access_token).toBe("at");
expect(r.refreshable).toBe(false);
}
});
});