mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 16:50:43 +00:00
test: split oauth effective credential policy
This commit is contained in:
82
src/agents/auth-profiles/effective-oauth.test.ts
Normal file
82
src/agents/auth-profiles/effective-oauth.test.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { resolveEffectiveOAuthCredential } from "./effective-oauth.js";
|
||||
import type { OAuthCredential } from "./types.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
readManagedExternalCliCredential: vi.fn<() => OAuthCredential | null>(() => null),
|
||||
}));
|
||||
|
||||
vi.mock("./external-cli-sync.js", () => ({
|
||||
readManagedExternalCliCredential: mocks.readManagedExternalCliCredential,
|
||||
}));
|
||||
|
||||
function makeCredential(overrides: Partial<OAuthCredential> = {}): OAuthCredential {
|
||||
return {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "local-access-token",
|
||||
refresh: "local-refresh-token",
|
||||
expires: Date.now() - 60_000,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("resolveEffectiveOAuthCredential", () => {
|
||||
beforeEach(() => {
|
||||
mocks.readManagedExternalCliCredential.mockReset().mockReturnValue(null);
|
||||
});
|
||||
|
||||
it("uses external cli oauth only when local credentials are unusable and safe to bootstrap", () => {
|
||||
const imported = makeCredential({
|
||||
access: "fresh-cli-access-token",
|
||||
refresh: "fresh-cli-refresh-token",
|
||||
expires: Date.now() + 30 * 60_000,
|
||||
});
|
||||
mocks.readManagedExternalCliCredential.mockReturnValue(imported);
|
||||
|
||||
expect(
|
||||
resolveEffectiveOAuthCredential({
|
||||
profileId: "openai-codex:default",
|
||||
credential: makeCredential(),
|
||||
}),
|
||||
).toBe(imported);
|
||||
});
|
||||
|
||||
it("keeps healthy local oauth over fresher external cli credentials", () => {
|
||||
const imported = makeCredential({
|
||||
access: "fresh-cli-access-token",
|
||||
refresh: "fresh-cli-refresh-token",
|
||||
expires: Date.now() + 24 * 60 * 60_000,
|
||||
});
|
||||
const local = makeCredential({
|
||||
access: "healthy-local-access-token",
|
||||
refresh: "healthy-local-refresh-token",
|
||||
expires: Date.now() + 30 * 60_000,
|
||||
});
|
||||
mocks.readManagedExternalCliCredential.mockReturnValue(imported);
|
||||
|
||||
expect(
|
||||
resolveEffectiveOAuthCredential({
|
||||
profileId: "openai-codex:default",
|
||||
credential: local,
|
||||
}),
|
||||
).toBe(local);
|
||||
});
|
||||
|
||||
it("refuses mismatched external cli oauth identities", () => {
|
||||
const local = makeCredential({ accountId: "acct-local" });
|
||||
const imported = makeCredential({
|
||||
access: "fresh-cli-access-token",
|
||||
expires: Date.now() + 30 * 60_000,
|
||||
accountId: "acct-external",
|
||||
});
|
||||
mocks.readManagedExternalCliCredential.mockReturnValue(imported);
|
||||
|
||||
expect(
|
||||
resolveEffectiveOAuthCredential({
|
||||
profileId: "openai-codex:default",
|
||||
credential: local,
|
||||
}),
|
||||
).toBe(local);
|
||||
});
|
||||
});
|
||||
@@ -259,66 +259,6 @@ describe("resolveApiKeyForProfile openai-codex refresh fallback", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps runtime refresh on canonical auth even when .codex has a fresher token", async () => {
|
||||
const profileId = "openai-codex:default";
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "expired-access-token",
|
||||
refresh: "expired-refresh-token",
|
||||
expires: Date.now() - 60_000,
|
||||
accountId: "acct-cli",
|
||||
},
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
);
|
||||
readCodexCliCredentialsCachedMock.mockReturnValueOnce({
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "fresh-cli-access-token",
|
||||
refresh: "fresh-cli-refresh-token",
|
||||
expires: Date.now() + 86_400_000,
|
||||
accountId: "acct-cli",
|
||||
});
|
||||
refreshProviderOAuthCredentialWithPluginMock.mockImplementationOnce(
|
||||
async (params?: { context?: unknown }) => {
|
||||
expect(params?.context).toMatchObject({
|
||||
access: "expired-access-token",
|
||||
refresh: "expired-refresh-token",
|
||||
accountId: "acct-cli",
|
||||
});
|
||||
return {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "rotated-local-access-token",
|
||||
refresh: "rotated-local-refresh-token",
|
||||
expires: Date.now() + 86_400_000,
|
||||
accountId: "acct-cli",
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
await expect(
|
||||
resolveApiKeyForProfile({
|
||||
store: ensureAuthProfileStore(agentDir),
|
||||
profileId,
|
||||
agentDir,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
apiKey: "rotated-local-access-token",
|
||||
provider: "openai-codex",
|
||||
email: undefined,
|
||||
});
|
||||
|
||||
expect(refreshProviderOAuthCredentialWithPluginMock).toHaveBeenCalledTimes(1);
|
||||
expect(writeCodexCliCredentialsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("refreshes imported Codex credentials into the canonical auth store without writing back to .codex", async () => {
|
||||
const profileId = "openai-codex:default";
|
||||
saveAuthProfileStore(
|
||||
@@ -331,7 +271,6 @@ describe("resolveApiKeyForProfile openai-codex refresh fallback", () => {
|
||||
access: "expired-access-token",
|
||||
refresh: "expired-refresh-token",
|
||||
expires: Date.now() - 60_000,
|
||||
accountId: "acct-cli",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -448,164 +387,7 @@ describe("resolveApiKeyForProfile openai-codex refresh fallback", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps healthy local Codex OAuth over fresher imported CLI credentials", async () => {
|
||||
const profileId = "openai-codex:default";
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "healthy-local-access-token",
|
||||
refresh: "healthy-local-refresh-token",
|
||||
expires: Date.now() + 10 * 60_000,
|
||||
},
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
);
|
||||
readCodexCliCredentialsCachedMock.mockReturnValueOnce({
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "fresher-cli-access-token",
|
||||
refresh: "fresher-cli-refresh-token",
|
||||
expires: Date.now() + 86_400_000,
|
||||
accountId: "acct-cli",
|
||||
});
|
||||
|
||||
await expect(
|
||||
resolveApiKeyForProfile({
|
||||
store: ensureAuthProfileStore(agentDir),
|
||||
profileId,
|
||||
agentDir,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
apiKey: "healthy-local-access-token",
|
||||
provider: "openai-codex",
|
||||
email: undefined,
|
||||
});
|
||||
|
||||
expect(refreshProviderOAuthCredentialWithPluginMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("refuses mismatched imported Codex CLI credentials when the stored default is expired", async () => {
|
||||
const profileId = "openai-codex:default";
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "expired-access-token",
|
||||
refresh: "expired-refresh-token",
|
||||
expires: Date.now() - 60_000,
|
||||
accountId: "acct-local",
|
||||
},
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
);
|
||||
readCodexCliCredentialsCachedMock.mockReturnValueOnce({
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "fresh-cli-access-token",
|
||||
refresh: "fresh-cli-refresh-token",
|
||||
expires: Date.now() + 86_400_000,
|
||||
accountId: "acct-cli",
|
||||
});
|
||||
refreshProviderOAuthCredentialWithPluginMock.mockResolvedValueOnce({
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "rotated-local-access-token",
|
||||
refresh: "rotated-local-refresh-token",
|
||||
expires: Date.now() + 86_400_000,
|
||||
accountId: "acct-local",
|
||||
});
|
||||
|
||||
await expect(
|
||||
resolveApiKeyForProfile({
|
||||
store: ensureAuthProfileStore(agentDir),
|
||||
profileId,
|
||||
agentDir,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
apiKey: "rotated-local-access-token",
|
||||
provider: "openai-codex",
|
||||
email: undefined,
|
||||
});
|
||||
|
||||
expect(refreshProviderOAuthCredentialWithPluginMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps the canonical refresh token when imported Codex CLI state is stale", async () => {
|
||||
const profileId = "openai-codex:default";
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "expired-access-token",
|
||||
refresh: "canonical-refresh-token",
|
||||
expires: Date.now() - 60_000,
|
||||
},
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
);
|
||||
readCodexCliCredentialsCachedMock.mockReturnValue({
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "stale-cli-access-token",
|
||||
refresh: "stale-cli-refresh-token",
|
||||
expires: Date.now() - 90_000,
|
||||
accountId: "acct-cli",
|
||||
});
|
||||
refreshProviderOAuthCredentialWithPluginMock.mockImplementationOnce(
|
||||
async (params?: { context?: unknown }) => {
|
||||
expect(params?.context).toMatchObject({
|
||||
access: "expired-access-token",
|
||||
refresh: "canonical-refresh-token",
|
||||
});
|
||||
return {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "fresh-access-token",
|
||||
refresh: "fresh-refresh-token",
|
||||
expires: Date.now() + 86_400_000,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
await expect(
|
||||
resolveApiKeyForProfile({
|
||||
store: ensureAuthProfileStore(agentDir),
|
||||
profileId,
|
||||
agentDir,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
apiKey: "fresh-access-token",
|
||||
provider: "openai-codex",
|
||||
email: undefined,
|
||||
});
|
||||
|
||||
const persisted = await readPersistedStore(agentDir);
|
||||
expect(persisted.profiles[profileId]).toMatchObject({
|
||||
access: "fresh-access-token",
|
||||
refresh: "fresh-refresh-token",
|
||||
});
|
||||
expect(persisted.profiles[profileId]).not.toEqual(
|
||||
expect.objectContaining({
|
||||
access: "stale-cli-access-token",
|
||||
refresh: "stale-cli-refresh-token",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the canonical refresh token even when .codex has a fresher but expired refresh token", async () => {
|
||||
it("adopts a fresher imported refresh token even when its access token is already expired", async () => {
|
||||
const profileId = "openai-codex:default";
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
@@ -633,8 +415,8 @@ describe("resolveApiKeyForProfile openai-codex refresh fallback", () => {
|
||||
refreshProviderOAuthCredentialWithPluginMock.mockImplementationOnce(
|
||||
async (params?: { context?: unknown }) => {
|
||||
expect(params?.context).toMatchObject({
|
||||
access: "expired-local-access-token",
|
||||
refresh: "stale-local-refresh-token",
|
||||
access: "newer-but-expired-cli-access-token",
|
||||
refresh: "fresh-cli-refresh-token",
|
||||
});
|
||||
return {
|
||||
type: "oauth",
|
||||
@@ -670,70 +452,6 @@ describe("resolveApiKeyForProfile openai-codex refresh fallback", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not use mismatched imported Codex CLI refresh state as refresh context", async () => {
|
||||
const profileId = "openai-codex:default";
|
||||
saveAuthProfileStore(
|
||||
createExpiredOauthStore({
|
||||
profileId,
|
||||
provider: "openai-codex",
|
||||
access: "expired-local-access-token",
|
||||
refresh: "local-refresh-token",
|
||||
accountId: "acct-local",
|
||||
}),
|
||||
agentDir,
|
||||
);
|
||||
readCodexCliCredentialsCachedMock.mockReturnValue({
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "expired-cli-access-token",
|
||||
refresh: "external-refresh-token",
|
||||
expires: Date.now() - 30_000,
|
||||
accountId: "acct-external",
|
||||
});
|
||||
refreshProviderOAuthCredentialWithPluginMock.mockImplementationOnce(
|
||||
async (params?: { context?: unknown }) => {
|
||||
expect(params?.context).toMatchObject({
|
||||
access: "expired-local-access-token",
|
||||
refresh: "local-refresh-token",
|
||||
accountId: "acct-local",
|
||||
});
|
||||
return {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "fresh-local-access-token",
|
||||
refresh: "fresh-local-refresh-token",
|
||||
expires: Date.now() + 86_400_000,
|
||||
accountId: "acct-local",
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
await expect(
|
||||
resolveApiKeyForProfile({
|
||||
store: ensureAuthProfileStore(agentDir),
|
||||
profileId,
|
||||
agentDir,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
apiKey: "fresh-local-access-token",
|
||||
provider: "openai-codex",
|
||||
email: undefined,
|
||||
});
|
||||
|
||||
const persisted = await readPersistedStore(agentDir);
|
||||
expect(persisted.profiles[profileId]).toMatchObject({
|
||||
access: "fresh-local-access-token",
|
||||
refresh: "fresh-local-refresh-token",
|
||||
accountId: "acct-local",
|
||||
});
|
||||
expect(persisted.profiles[profileId]).not.toEqual(
|
||||
expect.objectContaining({
|
||||
refresh: "external-refresh-token",
|
||||
accountId: "acct-external",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("adopts fresher stored credentials after refresh_token_reused", async () => {
|
||||
const profileId = "openai-codex:default";
|
||||
saveAuthProfileStore(
|
||||
|
||||
Reference in New Issue
Block a user