mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:10:43 +00:00
test: trim oauth adoption branch coverage
This commit is contained in:
@@ -12,12 +12,10 @@ import {
|
||||
} from "./store.js";
|
||||
import type { AuthProfileStore, OAuthCredential } from "./types.js";
|
||||
|
||||
// Cross-account-leak defense-in-depth: the three adopt sites in oauth.ts
|
||||
// now all call isSameOAuthIdentity before copying main-store credentials
|
||||
// into the sub-agent store. This suite exercises each of those sites
|
||||
// with a mismatched accountId on main vs. sub and asserts the adoption
|
||||
// is refused (sub store keeps its own credential; main's creds do not
|
||||
// leak through).
|
||||
// Cross-account-leak defense-in-depth: each adopt site in oauth.ts calls the
|
||||
// shared identity copy gate before copying main-store credentials into the
|
||||
// sub-agent store. Unit tests cover policy variants; this suite proves each
|
||||
// production branch refuses a mismatched accountId.
|
||||
|
||||
function resolveApiKeyForProfileInTest(
|
||||
params: Omit<Parameters<typeof resolveApiKeyForProfile>[0], "cfg">,
|
||||
@@ -276,356 +274,6 @@ describe("OAuth credential adoption is identity-gated", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("adoptNewerMainOAuthCredential still adopts when sub has no identity but main does (upgrade tolerance)", async () => {
|
||||
// Scenario: sub-agent stored its cred before accountId/email were
|
||||
// captured. Main has fresh cred with accountId. Under the STRICT rule
|
||||
// this would refuse (asymmetric). Under the relaxed rule used for
|
||||
// adoption it must allow — otherwise #26322 breaks for existing
|
||||
// installs on upgrade.
|
||||
const profileId = "openai-codex:default";
|
||||
const provider = "openai-codex";
|
||||
const subExpiry = Date.now() + 10 * 60 * 1000;
|
||||
const mainFresher = Date.now() + 60 * 60 * 1000;
|
||||
|
||||
const subAgentDir = path.join(tempRoot, "agents", "sub-upgrade", "agent");
|
||||
await fs.mkdir(subAgentDir, { recursive: true });
|
||||
saveAuthProfileStore(
|
||||
storeWith(
|
||||
profileId,
|
||||
oauthCred({
|
||||
provider,
|
||||
access: "sub-own-access",
|
||||
refresh: "sub-own-refresh",
|
||||
expires: subExpiry,
|
||||
// no accountId / email — pre-capture state
|
||||
}),
|
||||
),
|
||||
subAgentDir,
|
||||
);
|
||||
saveAuthProfileStore(
|
||||
storeWith(
|
||||
profileId,
|
||||
oauthCred({
|
||||
provider,
|
||||
access: "main-fresher-access",
|
||||
refresh: "main-fresher-refresh",
|
||||
expires: mainFresher,
|
||||
accountId: "acct-main",
|
||||
}),
|
||||
),
|
||||
mainAgentDir,
|
||||
);
|
||||
|
||||
const result = await resolveApiKeyForProfileInTest({
|
||||
store: ensureAuthProfileStore(subAgentDir),
|
||||
profileId,
|
||||
agentDir: subAgentDir,
|
||||
});
|
||||
|
||||
// Sub must have adopted main's fresher credential.
|
||||
expect(result?.apiKey).toBe("main-fresher-access");
|
||||
|
||||
const subRaw = JSON.parse(
|
||||
await fs.readFile(path.join(subAgentDir, "auth-profiles.json"), "utf8"),
|
||||
) as AuthProfileStore;
|
||||
expect(subRaw.profiles[profileId]).toMatchObject({
|
||||
access: "main-fresher-access",
|
||||
accountId: "acct-main",
|
||||
});
|
||||
});
|
||||
|
||||
it("inside-the-lock adopt tolerates sub-no-identity / main-has-identity (upgrade case)", async () => {
|
||||
// Same upgrade scenario but entering via the inside-lock adopt path:
|
||||
// sub cred is EXPIRED (forces entry into refreshOAuthTokenWithLock),
|
||||
// main has FRESH cred with accountId, sub has no identity.
|
||||
const profileId = "openai-codex:default";
|
||||
const provider = "openai-codex";
|
||||
const freshExpiry = Date.now() + 60 * 60 * 1000;
|
||||
|
||||
const subAgentDir = path.join(tempRoot, "agents", "sub-insidelock-upgrade", "agent");
|
||||
await fs.mkdir(subAgentDir, { recursive: true });
|
||||
saveAuthProfileStore(
|
||||
storeWith(
|
||||
profileId,
|
||||
oauthCred({
|
||||
provider,
|
||||
access: "sub-stale-access",
|
||||
refresh: "sub-stale-refresh",
|
||||
expires: Date.now() - 60_000,
|
||||
// no identity metadata
|
||||
}),
|
||||
),
|
||||
subAgentDir,
|
||||
);
|
||||
saveAuthProfileStore(
|
||||
storeWith(
|
||||
profileId,
|
||||
oauthCred({
|
||||
provider,
|
||||
access: "main-fresh-access",
|
||||
refresh: "main-fresh-refresh",
|
||||
expires: freshExpiry,
|
||||
accountId: "acct-main",
|
||||
}),
|
||||
),
|
||||
mainAgentDir,
|
||||
);
|
||||
|
||||
// Plugin refresh must NOT be called — sub should adopt main's fresh
|
||||
// cred rather than performing its own refresh.
|
||||
const result = await resolveApiKeyForProfileInTest({
|
||||
store: ensureAuthProfileStore(subAgentDir),
|
||||
profileId,
|
||||
agentDir: subAgentDir,
|
||||
});
|
||||
|
||||
expect(refreshProviderOAuthCredentialWithPluginMock).not.toHaveBeenCalled();
|
||||
expect(result?.apiKey).toBe("main-fresh-access");
|
||||
});
|
||||
|
||||
it("adoptNewerMainOAuthCredential refuses non-overlapping identity fields (sub has accountId, main has email)", async () => {
|
||||
// Reviewer-requested: with no COMPARABLE shared identity field there
|
||||
// is no positive-match evidence, so adoption must refuse.
|
||||
const profileId = "openai-codex:default";
|
||||
const provider = "openai-codex";
|
||||
const subExpiry = Date.now() + 10 * 60 * 1000;
|
||||
const mainFresher = Date.now() + 60 * 60 * 1000;
|
||||
|
||||
const subAgentDir = path.join(tempRoot, "agents", "sub-nonoverlap-pre", "agent");
|
||||
await fs.mkdir(subAgentDir, { recursive: true });
|
||||
saveAuthProfileStore(
|
||||
storeWith(
|
||||
profileId,
|
||||
oauthCred({
|
||||
provider,
|
||||
access: "sub-own-access",
|
||||
refresh: "sub-own-refresh",
|
||||
expires: subExpiry,
|
||||
accountId: "acct-sub",
|
||||
// NO email on sub
|
||||
}),
|
||||
),
|
||||
subAgentDir,
|
||||
);
|
||||
saveAuthProfileStore(
|
||||
storeWith(
|
||||
profileId,
|
||||
oauthCred({
|
||||
provider,
|
||||
access: "main-fresher-access",
|
||||
refresh: "main-fresher-refresh",
|
||||
expires: mainFresher,
|
||||
email: "main@example.com",
|
||||
// NO accountId on main
|
||||
}),
|
||||
),
|
||||
mainAgentDir,
|
||||
);
|
||||
|
||||
const result = await resolveApiKeyForProfileInTest({
|
||||
store: ensureAuthProfileStore(subAgentDir),
|
||||
profileId,
|
||||
agentDir: subAgentDir,
|
||||
});
|
||||
|
||||
// Sub must keep its own credential; pre-refresh adopt is refused.
|
||||
expect(result?.apiKey).toBe("sub-own-access");
|
||||
|
||||
const subRaw = JSON.parse(
|
||||
await fs.readFile(path.join(subAgentDir, "auth-profiles.json"), "utf8"),
|
||||
) as AuthProfileStore;
|
||||
expect(subRaw.profiles[profileId]).toMatchObject({
|
||||
access: "sub-own-access",
|
||||
accountId: "acct-sub",
|
||||
});
|
||||
});
|
||||
|
||||
it("inside-the-lock adopt refuses non-overlapping identity fields (sub has accountId, main has email)", async () => {
|
||||
const profileId = "openai-codex:default";
|
||||
const provider = "openai-codex";
|
||||
const freshExpiry = Date.now() + 60 * 60 * 1000;
|
||||
|
||||
const subAgentDir = path.join(tempRoot, "agents", "sub-nonoverlap-inside", "agent");
|
||||
await fs.mkdir(subAgentDir, { recursive: true });
|
||||
saveAuthProfileStore(
|
||||
storeWith(
|
||||
profileId,
|
||||
oauthCred({
|
||||
provider,
|
||||
access: "sub-stale-access",
|
||||
refresh: "sub-refresh-token",
|
||||
expires: Date.now() - 60_000,
|
||||
accountId: "acct-sub",
|
||||
}),
|
||||
),
|
||||
subAgentDir,
|
||||
);
|
||||
saveAuthProfileStore(
|
||||
storeWith(
|
||||
profileId,
|
||||
oauthCred({
|
||||
provider,
|
||||
access: "main-fresh-access",
|
||||
refresh: "main-fresh-refresh",
|
||||
expires: freshExpiry,
|
||||
email: "main@example.com",
|
||||
}),
|
||||
),
|
||||
mainAgentDir,
|
||||
);
|
||||
|
||||
refreshProviderOAuthCredentialWithPluginMock.mockImplementationOnce(
|
||||
async () =>
|
||||
({
|
||||
type: "oauth",
|
||||
provider,
|
||||
access: "sub-refreshed-access",
|
||||
refresh: "sub-refreshed-refresh",
|
||||
expires: freshExpiry,
|
||||
accountId: "acct-sub",
|
||||
}) as never,
|
||||
);
|
||||
|
||||
const result = await resolveApiKeyForProfileInTest({
|
||||
store: ensureAuthProfileStore(subAgentDir),
|
||||
profileId,
|
||||
agentDir: subAgentDir,
|
||||
});
|
||||
|
||||
// Sub performed its own refresh rather than adopting main's email-only cred.
|
||||
expect(refreshProviderOAuthCredentialWithPluginMock).toHaveBeenCalledTimes(1);
|
||||
expect(result?.apiKey).toBe("sub-refreshed-access");
|
||||
});
|
||||
|
||||
it("catch-block main-inherit refuses non-overlapping identity fields", async () => {
|
||||
const profileId = "openai-codex:default";
|
||||
const provider = "openai-codex";
|
||||
const freshExpiry = Date.now() + 60 * 60 * 1000;
|
||||
|
||||
const subAgentDir = path.join(tempRoot, "agents", "sub-nonoverlap-catch", "agent");
|
||||
await fs.mkdir(subAgentDir, { recursive: true });
|
||||
saveAuthProfileStore(
|
||||
storeWith(
|
||||
profileId,
|
||||
oauthCred({
|
||||
provider,
|
||||
access: "sub-stale-access",
|
||||
refresh: "sub-refresh-token",
|
||||
expires: Date.now() - 60_000,
|
||||
accountId: "acct-sub",
|
||||
}),
|
||||
),
|
||||
subAgentDir,
|
||||
);
|
||||
saveAuthProfileStore(
|
||||
storeWith(
|
||||
profileId,
|
||||
oauthCred({
|
||||
provider,
|
||||
access: "main-stale-access",
|
||||
refresh: "main-stale-refresh",
|
||||
expires: Date.now() - 60_000,
|
||||
email: "main@example.com",
|
||||
}),
|
||||
),
|
||||
mainAgentDir,
|
||||
);
|
||||
|
||||
refreshProviderOAuthCredentialWithPluginMock.mockImplementationOnce(async () => {
|
||||
// Another process writes a fresh email-only cred into main while
|
||||
// our refresh is in-flight, then we throw a generic upstream error.
|
||||
saveAuthProfileStore(
|
||||
storeWith(
|
||||
profileId,
|
||||
oauthCred({
|
||||
provider,
|
||||
access: "main-refreshed-access",
|
||||
refresh: "main-refreshed-refresh",
|
||||
expires: freshExpiry,
|
||||
email: "main@example.com",
|
||||
}),
|
||||
),
|
||||
mainAgentDir,
|
||||
);
|
||||
throw new Error("upstream 503 service unavailable");
|
||||
});
|
||||
|
||||
// Catch-block main-inherit must refuse the non-overlapping cred and
|
||||
// propagate the original error rather than leaking main's credential.
|
||||
await expect(
|
||||
resolveApiKeyForProfileInTest({
|
||||
store: ensureAuthProfileStore(subAgentDir),
|
||||
profileId,
|
||||
agentDir: subAgentDir,
|
||||
}),
|
||||
).rejects.toThrow(/OAuth token refresh failed for openai-codex/);
|
||||
});
|
||||
|
||||
it("catch-block main-inherit tolerates sub-no-identity / main-has-identity (upgrade case)", async () => {
|
||||
// Upgrade scenario hitting the catch-block fallback: sub refresh
|
||||
// throws, main later carries fresh cred with identity. Sub must
|
||||
// inherit rather than surface the error to the caller.
|
||||
const profileId = "openai-codex:default";
|
||||
const provider = "openai-codex";
|
||||
const freshExpiry = Date.now() + 60 * 60 * 1000;
|
||||
|
||||
const subAgentDir = path.join(tempRoot, "agents", "sub-catch-upgrade", "agent");
|
||||
await fs.mkdir(subAgentDir, { recursive: true });
|
||||
saveAuthProfileStore(
|
||||
storeWith(
|
||||
profileId,
|
||||
oauthCred({
|
||||
provider,
|
||||
access: "sub-stale-access",
|
||||
refresh: "sub-stale-refresh",
|
||||
expires: Date.now() - 60_000,
|
||||
// no identity metadata
|
||||
}),
|
||||
),
|
||||
subAgentDir,
|
||||
);
|
||||
saveAuthProfileStore(
|
||||
storeWith(
|
||||
profileId,
|
||||
oauthCred({
|
||||
provider,
|
||||
access: "main-stale-access",
|
||||
refresh: "main-stale-refresh",
|
||||
expires: Date.now() - 60_000,
|
||||
accountId: "acct-main",
|
||||
}),
|
||||
),
|
||||
mainAgentDir,
|
||||
);
|
||||
|
||||
refreshProviderOAuthCredentialWithPluginMock.mockImplementationOnce(async () => {
|
||||
// Another process refreshes main while our refresh is in flight.
|
||||
saveAuthProfileStore(
|
||||
storeWith(
|
||||
profileId,
|
||||
oauthCred({
|
||||
provider,
|
||||
access: "main-refreshed-access",
|
||||
refresh: "main-refreshed-refresh",
|
||||
expires: freshExpiry,
|
||||
accountId: "acct-main",
|
||||
}),
|
||||
),
|
||||
mainAgentDir,
|
||||
);
|
||||
throw new Error("upstream 503 service unavailable");
|
||||
});
|
||||
|
||||
const result = await resolveApiKeyForProfileInTest({
|
||||
store: ensureAuthProfileStore(subAgentDir),
|
||||
profileId,
|
||||
agentDir: subAgentDir,
|
||||
});
|
||||
|
||||
// Sub inherited main's fresh cred via the catch-block fallback.
|
||||
expect(result?.apiKey).toBe("main-refreshed-access");
|
||||
});
|
||||
|
||||
it("catch-block main-inherit refuses across accountId mismatch and surfaces the original error", async () => {
|
||||
// Scenario: sub-agent refresh throws a non-refresh_token_reused error.
|
||||
// Main has fresh creds for a DIFFERENT account. The catch-block
|
||||
|
||||
Reference in New Issue
Block a user