Files
TJWaterAgent/tests/auth/credentialRefresh.test.ts

101 lines
3.5 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import {
CredentialRefreshCoordinator,
CredentialRefreshError,
runWithCredentialRefresh,
} from "../../src/auth/credentialRefresh.js";
import { type RuntimeSessionContext } from "../../src/runtime/sessionContext.js";
const context = (overrides: Partial<RuntimeSessionContext> = {}) => ({
accessToken: "old-token",
actorKey: "user-1",
clientSessionId: "client-1",
projectId: "project-1",
projectKey: "project-1",
sessionId: "session-1",
traceId: "trace-1",
...overrides,
});
describe("CredentialRefreshCoordinator", () => {
test("deduplicates concurrent refreshes for one session", async () => {
const coordinator = new CredentialRefreshCoordinator();
const requestIds: string[] = [];
coordinator.subscribe("session-1", (event) => {
if (event.type === "credential_refresh_required") {
requestIds.push(event.requestId);
}
});
const expired = context({ tokenExpiresAt: new Date(0).toISOString() });
const execute = async (active: RuntimeSessionContext) => ({
status: 200,
token: active.accessToken,
});
const first = runWithCredentialRefresh(coordinator, expired, execute);
const second = runWithCredentialRefresh(coordinator, expired, execute);
await Promise.resolve();
expect(requestIds).toHaveLength(1);
expect(coordinator.getPendingEvent("session-1")).toMatchObject({
type: "credential_refresh_required",
requestId: requestIds[0],
reason: "access_token_expired",
});
coordinator.resolve(
"session-1",
requestIds[0]!,
context({ accessToken: "fresh-token" }),
);
expect(await first).toEqual({ status: 200, token: "fresh-token" });
expect(await second).toEqual({ status: 200, token: "fresh-token" });
expect(coordinator.getPendingEvent("session-1")).toBeNull();
});
test("retries one time on 401 and never refreshes a 403", async () => {
const coordinator = new CredentialRefreshCoordinator();
let requestId = "";
coordinator.subscribe("session-1", (event) => {
if (event.type === "credential_refresh_required") {
requestId = event.requestId;
}
});
let attempts = 0;
const resultPromise = runWithCredentialRefresh(
coordinator,
context(),
async () => ({ status: ++attempts === 1 ? 401 : 401 }),
);
await Promise.resolve();
coordinator.resolve("session-1", requestId, context({ accessToken: "fresh-token" }));
expect((await resultPromise).status).toBe(401);
expect(attempts).toBe(2);
requestId = "";
expect(
(await runWithCredentialRefresh(coordinator, context(), async () => ({ status: 403 })))
.status,
).toBe(403);
expect(requestId).toBe("");
});
test("fails explicitly when no event stream can refresh credentials", async () => {
await expect(
runWithCredentialRefresh(
new CredentialRefreshCoordinator(),
context({ tokenExpiresAt: new Date(0).toISOString() }),
async () => ({ status: 200 }),
),
).rejects.toBeInstanceOf(CredentialRefreshError);
});
test("reports run cancellation separately from authentication failure", async () => {
const coordinator = new CredentialRefreshCoordinator();
coordinator.subscribe("session-1", () => undefined);
const pending = coordinator.request("session-1", "access_token_rejected");
coordinator.cancelSession("session-1");
await expect(pending).rejects.toMatchObject({ code: "cancelled" });
});
});