import { afterEach, beforeEach, describe, expect, test, vi } from "bun:test";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import {
type AuthCredentialStore,
AuthStorage,
REMOTE_REFRESH_SENTINEL,
SqliteAuthCredentialStore,
} from "../src/auth-storage";
import * as oauthUtils from "../src/utils/oauth";
describe("AuthStorage broker sentinel refresh", () => {
let tempDir = "";
let store: AuthCredentialStore | undefined;
let authStorage: AuthStorage | undefined;
let brokerRefreshCalls = 0;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "pi-ai-auth-broker-no-sentinel-"));
store = await SqliteAuthCredentialStore.open(path.join(tempDir, "agent.db"));
brokerRefreshCalls = 0;
authStorage = new AuthStorage(store, {
refreshOAuthCredential: async (_provider, credentialId, credential) => {
brokerRefreshCalls += 1;
expect(credentialId).toBe(1);
expect(credential.refresh).toBe(REMOTE_REFRESH_SENTINEL);
return {
access: "broker-access-rotated",
refresh: REMOTE_REFRESH_SENTINEL,
expires: Date.now() + 60 * 60_000,
accountId: "broker-account",
email: "broker@example.com",
projectId: "broker-project",
};
},
});
});
afterEach(async () => {
vi.restoreAllMocks();
store?.close();
store = undefined;
authStorage = undefined;
if (tempDir) {
await fs.rm(tempDir, { recursive: true, force: true });
tempDir = "";
}
});
test("getOAuthAccess refreshes expired broker credentials through the store hook only", async () => {
if (!authStorage || !store) throw new Error("test setup failed");
await authStorage.set("anthropic", [
{
type: "oauth",
access: "broker-access-stale",
refresh: REMOTE_REFRESH_SENTINEL,
expires: Date.now() - 60_000,
accountId: "broker-account-old",
},
]);
const providerRefresh = vi.spyOn(oauthUtils, "refreshOAuthToken").mockImplementation(async () => {
throw new Error("provider-direct refresh must not be called");
});
const access = await authStorage.getOAuthAccess("anthropic", "broker-session");
expect(access).toEqual({
accessToken: "broker-access-rotated",
accountId: "broker-account",
email: "broker@example.com",
projectId: "broker-project",
enterpriseUrl: undefined,
});
expect(brokerRefreshCalls).toBe(1);
expect(providerRefresh).not.toHaveBeenCalled();
const persisted = store.listAuthCredentials("anthropic");
expect(persisted).toHaveLength(1);
expect(persisted[0]?.credential.type).toBe("oauth");
if (persisted[0]?.credential.type === "oauth") {
expect(persisted[0].credential.access).toBe("broker-access-rotated");
expect(persisted[0].credential.refresh).toBe(REMOTE_REFRESH_SENTINEL);
}
});
test("getOAuthApiKey refuses expired broker sentinels instead of provider-direct refresh", async () => {
const providerRefresh = vi.spyOn(oauthUtils, "refreshOAuthToken").mockImplementation(async () => {
throw new Error("provider-direct refresh must not be called");
});
await expect(
oauthUtils.getOAuthApiKey("anthropic", {
anthropic: {
access: "broker-access-stale",
refresh: REMOTE_REFRESH_SENTINEL,
expires: Date.now() - 60_000,
},
}),
).rejects.toThrow("must be refreshed via AuthStorage");
expect(providerRefresh).not.toHaveBeenCalled();
});
});