185 lines
7.4 KiB
TypeScript
185 lines
7.4 KiB
TypeScript
import { afterEach, describe, expect, test } from "bun:test";
|
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { resolveToken, type GitHubYamlAuthConfig } from "./gh/auth-and-safety";
|
|
import { authRequired } from "./gh/client";
|
|
import { resolvePositionalIssueReference, resolvePositionalPrReference } from "./gh/refs";
|
|
import type { GitHubOptions, GitHubResolvedNumberReference } from "./gh/types";
|
|
|
|
const originalGhToken = process.env.GH_TOKEN;
|
|
const originalGithubToken = process.env.GITHUB_TOKEN;
|
|
const projectedEnv = "UNIDESK_TEST_GH_REPOSITORY_OVERRIDE";
|
|
const originalProjectedToken = process.env[projectedEnv];
|
|
const temporaryRoots: string[] = [];
|
|
|
|
afterEach(() => {
|
|
if (originalGhToken === undefined) delete process.env.GH_TOKEN;
|
|
else process.env.GH_TOKEN = originalGhToken;
|
|
if (originalGithubToken === undefined) delete process.env.GITHUB_TOKEN;
|
|
else process.env.GITHUB_TOKEN = originalGithubToken;
|
|
if (originalProjectedToken === undefined) delete process.env[projectedEnv];
|
|
else process.env[projectedEnv] = originalProjectedToken;
|
|
while (temporaryRoots.length > 0) rmSync(temporaryRoots.pop()!, { recursive: true, force: true });
|
|
});
|
|
|
|
function testAuthConfig(): GitHubYamlAuthConfig {
|
|
const root = mkdtempSync("/tmp/unidesk-gh-token-override-");
|
|
temporaryRoots.push(root);
|
|
writeFileSync(join(root, "global-token.txt"), "global-token\n");
|
|
writeFileSync(join(root, "selfmedia-token.txt"), "selfmedia-token\n");
|
|
return {
|
|
tokenSource: {
|
|
enabled: true,
|
|
priority: "before-env",
|
|
root,
|
|
sourceRef: "global-token.txt",
|
|
sourceKey: "GH_TOKEN",
|
|
format: "raw-token",
|
|
scope: "global",
|
|
configRef: "test#github.auth.tokenSource",
|
|
},
|
|
repositoryOverrides: [{
|
|
repository: "pikainc/selfmedia",
|
|
priority: "before-env",
|
|
root,
|
|
sourceRef: "selfmedia-token.txt",
|
|
sourceKey: "GH_TOKEN",
|
|
format: "raw-token",
|
|
scope: "repository-override",
|
|
configRef: "test#github.auth.repositoryOverrides[0]",
|
|
}],
|
|
};
|
|
}
|
|
|
|
function options(repo: string): GitHubOptions {
|
|
return { repo } as GitHubOptions;
|
|
}
|
|
|
|
function resolvedRepo(value: GitHubResolvedNumberReference | { ok: boolean }): string {
|
|
if (!("number" in value)) throw new Error("expected a resolved positional reference");
|
|
return value.repo;
|
|
}
|
|
|
|
describe("GitHub repository token overrides", () => {
|
|
test("selects the exact repository override without affecting global repositories", () => {
|
|
delete process.env.GH_TOKEN;
|
|
delete process.env.GITHUB_TOKEN;
|
|
const authConfig = testAuthConfig();
|
|
|
|
const selfmedia = resolveToken("pikainc/selfmedia", false, authConfig);
|
|
const unidesk = resolveToken("pikasTech/unidesk", false, authConfig);
|
|
const other = resolveToken("owner/other", false, authConfig);
|
|
|
|
expect(selfmedia.token).toBe("selfmedia-token");
|
|
expect(selfmedia.probe).toMatchObject({ scope: "repository-override", sourceRef: "selfmedia-token.txt", sourceKey: "GH_TOKEN", valuesPrinted: false });
|
|
expect(unidesk.token).toBe("global-token");
|
|
expect(other.token).toBe("global-token");
|
|
expect(unidesk.probe).toMatchObject({ scope: "global", sourceRef: "global-token.txt", valuesPrinted: false });
|
|
expect(other.probe).toMatchObject({ scope: "global", sourceRef: "global-token.txt", valuesPrinted: false });
|
|
expect(selfmedia.probe.tokenFingerprint).not.toBe(unidesk.probe.tokenFingerprint);
|
|
});
|
|
|
|
test("keeps before-env repository overrides ahead of global environment tokens", () => {
|
|
process.env.GH_TOKEN = "environment-token";
|
|
process.env.GITHUB_TOKEN = "secondary-environment-token";
|
|
|
|
const selected = resolveToken("pikainc/selfmedia", false, testAuthConfig());
|
|
|
|
expect(selected.token).toBe("selfmedia-token");
|
|
expect(selected.probe).toMatchObject({ source: "yaml-token-source", scope: "repository-override", yamlSourcePriority: "before-env" });
|
|
});
|
|
|
|
test("uses the YAML-declared projected environment for repository overrides", () => {
|
|
process.env.GH_TOKEN = "global-environment-token";
|
|
process.env[projectedEnv] = "projected-repository-token";
|
|
const authConfig = testAuthConfig();
|
|
authConfig.repositoryOverrides[0] = {
|
|
...authConfig.repositoryOverrides[0],
|
|
sourceRef: "missing-token.txt",
|
|
projectedEnv,
|
|
};
|
|
|
|
const selected = resolveToken("pikainc/selfmedia", false, authConfig);
|
|
|
|
expect(selected.token).toBe("projected-repository-token");
|
|
expect(selected.probe).toMatchObject({
|
|
source: "yaml-token-projection",
|
|
scope: "repository-override",
|
|
sourceExists: false,
|
|
sourceKeyPresent: false,
|
|
projectedEnv,
|
|
projectedEnvPresent: true,
|
|
valuesPrinted: false,
|
|
});
|
|
});
|
|
|
|
test("fails closed when a declared repository override is missing", () => {
|
|
process.env.GH_TOKEN = "global-environment-token";
|
|
delete process.env[projectedEnv];
|
|
const authConfig = testAuthConfig();
|
|
authConfig.repositoryOverrides[0] = {
|
|
...authConfig.repositoryOverrides[0],
|
|
sourceRef: "missing-token.txt",
|
|
projectedEnv,
|
|
};
|
|
|
|
const selected = resolveToken("pikainc/selfmedia", true, authConfig);
|
|
const failure = authRequired("pikainc/selfmedia", "pr view", selected.probe);
|
|
|
|
expect(selected.token).toBeNull();
|
|
expect(selected.probe).toMatchObject({
|
|
present: false,
|
|
scope: "repository-override",
|
|
sourceExists: false,
|
|
sourceKeyPresent: false,
|
|
projectedEnv,
|
|
projectedEnvPresent: false,
|
|
ghFallbackAttempted: false,
|
|
valuesPrinted: false,
|
|
});
|
|
expect(failure).toMatchObject({
|
|
ok: false,
|
|
degradedReason: "missing-token",
|
|
runnerDisposition: "infra-blocked",
|
|
token: {
|
|
scope: "repository-override",
|
|
projectedEnvPresent: false,
|
|
valuesPrinted: false,
|
|
},
|
|
});
|
|
expect(JSON.stringify(failure)).not.toContain("global-environment-token");
|
|
});
|
|
|
|
test("matches repository overrides without case sensitivity", () => {
|
|
delete process.env.GH_TOKEN;
|
|
delete process.env.GITHUB_TOKEN;
|
|
const authConfig = testAuthConfig();
|
|
|
|
for (const repo of ["PikaInc/selfmedia", "pikainc/SELFMEDIA", "PIKAINC/SELFMEDIA"]) {
|
|
const selected = resolveToken(repo, false, authConfig);
|
|
expect(selected.token).toBe("selfmedia-token");
|
|
expect(selected.probe).toMatchObject({ scope: "repository-override", sourceRef: "selfmedia-token.txt", valuesPrinted: false });
|
|
}
|
|
|
|
const global = resolveToken("PikasTech/UniDesk", false, authConfig);
|
|
expect(global.token).toBe("global-token");
|
|
expect(global.probe).toMatchObject({ scope: "global", sourceRef: "global-token.txt", valuesPrinted: false });
|
|
});
|
|
|
|
test("uses the final positional repository for issue and PR token selection", () => {
|
|
delete process.env.GH_TOKEN;
|
|
delete process.env.GITHUB_TOKEN;
|
|
const authConfig = testAuthConfig();
|
|
const issueRef = resolvePositionalIssueReference(["issue", "edit", "pikainc/selfmedia#12"], 2, "issue edit", options("pikasTech/unidesk"));
|
|
const prRef = resolvePositionalPrReference(["pr", "comment", "pikainc/selfmedia#34"], 2, "pr comment", options("pikasTech/unidesk"));
|
|
|
|
const issueToken = resolveToken(resolvedRepo(issueRef), false, authConfig);
|
|
const prToken = resolveToken(resolvedRepo(prRef), false, authConfig);
|
|
const defaultToken = resolveToken("pikasTech/unidesk", false, authConfig);
|
|
|
|
expect(issueToken.token).toBe("selfmedia-token");
|
|
expect(prToken.token).toBe("selfmedia-token");
|
|
expect(defaultToken.token).toBe("global-token");
|
|
});
|
|
});
|