Files
9router/tests/unit/codebuddy-intl-backfill.test.js
MUH. IQRAM BAHRING 604b4d85d5 fix(codebuddy-intl): probe token in connection test + name OAuth by identity
Two bugs on codebuddy-intl connections:

1. Test Connection always failed with "Provider test not supported":
   codebuddy-intl was missing from OAUTH_TEST_CONFIG, so testOAuthConnection
   bailed before probing. Add a real probe against the Keycloak realm's
   userinfo endpoint (URL derived from the token's iss claim), and wire
   refreshable so an expired token is rotated via refreshCodebuddyIntlToken.

2. OAuth logins were named "Account N" with no email: mapTokens returned no
   identity, even though the access token is a Keycloak JWT carrying
   email/name claims. Extract email + displayName in mapTokens (new shared
   extractDisplayNameFromAccessToken helper) so fresh logins are named and
   deduped by identity.

Also add a run-once backfill (backfillCodeBuddyIntlIdentity) invoked from
GET /api/providers and /api/providers/client to self-heal existing rows
(backfill email/displayName, rename the generic "Account N" placeholder).

Verified live: the real connection now returns valid:true and the row is
renamed to the account email.
2026-09-19 12:14:21 +08:00

120 lines
3.8 KiB
JavaScript

// Existing CodeBuddy Intl OAuth connections created before mapTokens surfaced
// identity show up as "Account N" with no email. The self-healing backfill must
// fill email + displayName from the access-token JWT so the dashboard shows the
// real identity without forcing a re-login.
//
// backfillCodeBuddyIntlIdentity has a module-level run-once guard, so each test
// re-imports a fresh module instance via vi.resetModules() + dynamic import.
import { describe, it, expect, beforeEach, vi } from "vitest";
const mocks = vi.hoisted(() => ({
getProviderConnections: vi.fn(),
updateProviderConnection: vi.fn(),
}));
vi.mock("@/lib/localDb", () => ({
getProviderConnections: mocks.getProviderConnections,
updateProviderConnection: mocks.updateProviderConnection,
}));
// The providers index imports open-sse/index.js for proxy-aware fetch; stub it.
vi.mock("open-sse/index.js", () => ({}));
function makeJwt(payload) {
const b64 = (obj) => Buffer.from(JSON.stringify(obj)).toString("base64url");
return `${b64({ alg: "RS256", typ: "JWT" })}.${b64(payload)}.sig`;
}
const JWT = makeJwt({
iss: "https://www.codebuddy.ai/auth/realms/copilot",
email: "aghiyaramadh@gmail.com",
name: "aghiya ramadh",
});
async function loadBackfill() {
vi.resetModules();
const mod = await import("../../src/lib/oauth/providers/index.js");
return mod.backfillCodeBuddyIntlIdentity;
}
describe("backfillCodeBuddyIntlIdentity", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.updateProviderConnection.mockResolvedValue({});
});
it("fills email + displayName for a legacy 'Account N' connection", async () => {
mocks.getProviderConnections.mockResolvedValue([
{
id: "conn-legacy",
provider: "codebuddy-intl",
authType: "oauth",
name: "Account 1",
email: null,
displayName: null,
accessToken: JWT,
},
]);
const backfill = await loadBackfill();
await backfill();
expect(mocks.updateProviderConnection).toHaveBeenCalledTimes(1);
const [id, patch] = mocks.updateProviderConnection.mock.calls[0];
expect(id).toBe("conn-legacy");
expect(patch.email).toBe("aghiyaramadh@gmail.com");
expect(patch.displayName).toBe("aghiya ramadh");
// Generic placeholder name is replaced by the identity.
expect(patch.name).toBe("aghiyaramadh@gmail.com");
});
it("keeps a user-customized name (does not overwrite non-generic names)", async () => {
mocks.getProviderConnections.mockResolvedValue([
{
id: "conn-custom",
provider: "codebuddy-intl",
authType: "oauth",
name: "My Work Account",
email: null,
displayName: null,
accessToken: JWT,
},
]);
const backfill = await loadBackfill();
await backfill();
expect(mocks.updateProviderConnection).toHaveBeenCalledTimes(1);
const [, patch] = mocks.updateProviderConnection.mock.calls[0];
expect(patch.email).toBe("aghiyaramadh@gmail.com");
expect(patch.name).toBeUndefined();
});
it("leaves connections that already have identity untouched", async () => {
mocks.getProviderConnections.mockResolvedValue([
{
id: "conn-ok",
provider: "codebuddy-intl",
authType: "oauth",
name: "aghiya ramadh",
email: "aghiyaramadh@gmail.com",
displayName: "aghiya ramadh",
accessToken: JWT,
},
]);
const backfill = await loadBackfill();
await backfill();
expect(mocks.updateProviderConnection).not.toHaveBeenCalled();
});
it("ignores other providers", async () => {
mocks.getProviderConnections.mockResolvedValue([
{ id: "c1", provider: "codex", authType: "oauth", email: null, accessToken: JWT },
]);
const backfill = await loadBackfill();
await backfill();
expect(mocks.updateProviderConnection).not.toHaveBeenCalled();
});
});