Files
openclaw/extensions/github-copilot/login.test.ts
Tobias Oort 0307deacfa feat(github-copilot): support GitHub Enterprise data-residency Copilot auth (#99221)
Adds GitHub Enterprise data-residency support to the existing bundled GitHub Copilot provider.

Maintainer proof:
- GitHub CI green on head 54010a6538
- `check-lint`, `check-additional-extension-bundled`, and `check-shrinkwrap` passed in CI
- local `pnpm lint:extensions:bundled`, `pnpm lint`, and focused GitHub Copilot Vitest passed
- AWS Crabbox proof passed
- live microsoft.ghe.com device-flow/token-exchange/model-catalog proof passed

Co-authored-by: Tobias Oort <tobias.oort@ict.nl>
Co-authored-by: Gio Della-Libera <235387111+giodl73-repo@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-08 18:59:41 -07:00

290 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Github Copilot tests cover device-flow login behavior.
import { afterEach, describe, expect, it, vi } from "vitest";
import {
runGitHubCopilotDeviceFlow,
setGitHubCopilotDeviceFlowFetchGuardForTesting,
withGithubCopilotDomainConfig,
} from "./login.js";
const DEVICE_CODE_URL = "https://github.com/login/device/code";
const ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
// A valid device code payload GitHub returns on the first step.
const VALID_DEVICE_CODE_BODY = {
device_code: "dev-code-abc123",
user_code: "ABCD-1234",
verification_uri: "https://github.com/login/device",
expires_in: 900,
interval: 5,
};
function guardResponse(body: unknown, status = 200, url = DEVICE_CODE_URL) {
return {
response: new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
}),
finalUrl: url,
release: vi.fn(async () => {}),
};
}
afterEach(() => {
setGitHubCopilotDeviceFlowFetchGuardForTesting(null);
vi.restoreAllMocks();
});
describe("runGitHubCopilotDeviceFlow — normal flow", () => {
it("returns authorized status and access token on successful flow", async () => {
let callIdx = 0;
setGitHubCopilotDeviceFlowFetchGuardForTesting(async (params) => {
callIdx += 1;
if (callIdx === 1) {
expect(params.url).toBe(DEVICE_CODE_URL);
return guardResponse(VALID_DEVICE_CODE_BODY);
}
expect(params.url).toBe(ACCESS_TOKEN_URL);
return guardResponse(
{ access_token: "ghu_tok_xyz", token_type: "bearer" },
200,
ACCESS_TOKEN_URL,
);
});
const showCode = vi.fn(async () => {});
const result = await runGitHubCopilotDeviceFlow({ showCode });
expect(result).toEqual({ status: "authorized", accessToken: "ghu_tok_xyz" });
expect(showCode).toHaveBeenCalledWith({
verificationUrl: "https://github.com/login/device",
userCode: "ABCD-1234",
expiresInMs: expect.any(Number),
});
expect(callIdx).toBe(2);
});
it("returns access_denied when GitHub rejects the authorization", async () => {
let callIdx = 0;
setGitHubCopilotDeviceFlowFetchGuardForTesting(async () => {
callIdx += 1;
if (callIdx === 1) {
return guardResponse(VALID_DEVICE_CODE_BODY);
}
return guardResponse({ error: "access_denied" }, 200, ACCESS_TOKEN_URL);
});
const result = await runGitHubCopilotDeviceFlow({
showCode: vi.fn(async () => {}),
});
expect(result).toEqual({ status: "access_denied" });
});
it("returns expired when GitHub reports expired_token", async () => {
let callIdx = 0;
setGitHubCopilotDeviceFlowFetchGuardForTesting(async () => {
callIdx += 1;
if (callIdx === 1) {
return guardResponse(VALID_DEVICE_CODE_BODY);
}
return guardResponse({ error: "expired_token" }, 200, ACCESS_TOKEN_URL);
});
const result = await runGitHubCopilotDeviceFlow({
showCode: vi.fn(async () => {}),
});
expect(result).toEqual({ status: "expired" });
});
});
describe("runGitHubCopilotDeviceFlow — HTTP error propagation", () => {
it("throws with failureLabel on non-OK device code response", async () => {
setGitHubCopilotDeviceFlowFetchGuardForTesting(async () => guardResponse({}, 401));
await expect(runGitHubCopilotDeviceFlow({ showCode: vi.fn() })).rejects.toThrow(
"GitHub device code failed: HTTP 401",
);
});
it("throws with failureLabel on non-OK access token response", async () => {
let callIdx = 0;
setGitHubCopilotDeviceFlowFetchGuardForTesting(async () => {
callIdx += 1;
if (callIdx === 1) {
return guardResponse(VALID_DEVICE_CODE_BODY);
}
return guardResponse({}, 500, ACCESS_TOKEN_URL);
});
await expect(runGitHubCopilotDeviceFlow({ showCode: vi.fn(async () => {}) })).rejects.toThrow(
"GitHub device token failed: HTTP 500",
);
});
});
describe("postGitHubDeviceFlowForm — response size bound", () => {
it("bounds oversized device code body and cancels the stream", async () => {
const chunk = new Uint8Array(1024 * 1024); // 1 MiB
let readCount = 0;
let canceled = false;
// 64 chunks × 1 MiB = 64 MiB — far exceeds the 16 MiB cap
const oversizedBody = new ReadableStream<Uint8Array>({
pull(controller) {
if (readCount >= 64) {
controller.close();
return;
}
readCount += 1;
controller.enqueue(chunk);
},
cancel() {
canceled = true;
},
});
setGitHubCopilotDeviceFlowFetchGuardForTesting(async () => ({
response: new Response(oversizedBody, {
status: 200,
headers: { "Content-Type": "application/json" },
}),
finalUrl: DEVICE_CODE_URL,
release: async () => {},
}));
await expect(runGitHubCopilotDeviceFlow({ showCode: vi.fn() })).rejects.toThrow(
"github-copilot.device-flow",
);
// Stream must be cancelled before all 64 MiB are consumed
expect(readCount).toBeLessThan(64);
expect(canceled).toBe(true);
});
it("bounds oversized access token body and cancels the stream", async () => {
const chunk = new Uint8Array(1024 * 1024); // 1 MiB
let readCount = 0;
let canceled = false;
let callIdx = 0;
setGitHubCopilotDeviceFlowFetchGuardForTesting(async () => {
callIdx += 1;
if (callIdx === 1) {
return guardResponse(VALID_DEVICE_CODE_BODY);
}
const oversizedBody = new ReadableStream<Uint8Array>({
pull(controller) {
if (readCount >= 64) {
controller.close();
return;
}
readCount += 1;
controller.enqueue(chunk);
},
cancel() {
canceled = true;
},
});
return {
response: new Response(oversizedBody, {
status: 200,
headers: { "Content-Type": "application/json" },
}),
finalUrl: ACCESS_TOKEN_URL,
release: async () => {},
};
});
await expect(runGitHubCopilotDeviceFlow({ showCode: vi.fn(async () => {}) })).rejects.toThrow(
"github-copilot.device-flow",
);
// Stream must be cancelled before all 64 MiB are consumed
expect(readCount).toBeLessThan(64);
expect(canceled).toBe(true);
});
});
describe("runGitHubCopilotDeviceFlow — data-residency GitHub Enterprise", () => {
const GHE_DOMAIN = "acme.ghe.com";
it("targets the enterprise device-flow endpoints when a domain is provided", async () => {
const gheDeviceCodeUrl = `https://${GHE_DOMAIN}/login/device/code`;
const gheAccessTokenUrl = `https://${GHE_DOMAIN}/login/oauth/access_token`;
const urls: string[] = [];
let callIdx = 0;
setGitHubCopilotDeviceFlowFetchGuardForTesting(async (params) => {
urls.push(params.url);
callIdx += 1;
if (callIdx === 1) {
expect(params.policy).toEqual({ hostnameAllowlist: [GHE_DOMAIN] });
return guardResponse(
{ ...VALID_DEVICE_CODE_BODY, verification_uri: `https://${GHE_DOMAIN}/login/device` },
200,
gheDeviceCodeUrl,
);
}
return guardResponse(
{ access_token: "ghu_ghe_tok", token_type: "bearer" },
200,
gheAccessTokenUrl,
);
});
const showCode = vi.fn(async () => {});
const result = await runGitHubCopilotDeviceFlow({ showCode }, GHE_DOMAIN);
expect(result).toEqual({ status: "authorized", accessToken: "ghu_ghe_tok" });
expect(urls).toEqual([gheDeviceCodeUrl, gheAccessTokenUrl]);
expect(showCode).toHaveBeenCalledWith({
verificationUrl: `https://${GHE_DOMAIN}/login/device`,
userCode: "ABCD-1234",
expiresInMs: expect.any(Number),
});
});
it("rejects a verification URL whose host does not match the configured domain", async () => {
setGitHubCopilotDeviceFlowFetchGuardForTesting(async () =>
guardResponse(
{ ...VALID_DEVICE_CODE_BODY, verification_uri: "https://github.com/login/device" },
200,
`https://${GHE_DOMAIN}/login/device/code`,
),
);
await expect(
runGitHubCopilotDeviceFlow({ showCode: vi.fn(async () => {}) }, GHE_DOMAIN),
).rejects.toThrow("unexpected verification URL");
});
});
describe("withGithubCopilotDomainConfig — shortcut login domain persistence", () => {
const tenantConfig = {
models: {
providers: { "github-copilot": { params: { githubDomain: "acme.ghe.com" } } },
},
} as never;
it("persists the tenant domain when the shortcut minted a tenant token", () => {
const next = withGithubCopilotDomainConfig({} as never, "acme.ghe.com");
expect(
(next as { models?: { providers?: Record<string, { params?: Record<string, unknown> }> } })
.models?.providers?.["github-copilot"]?.params?.githubDomain,
).toBe("acme.ghe.com");
});
it("clears a stale tenant domain when the shortcut logged in against github.com", () => {
const next = withGithubCopilotDomainConfig(tenantConfig, "github.com");
const params = (
next as { models?: { providers?: Record<string, { params?: Record<string, unknown> }> } }
).models?.providers?.["github-copilot"]?.params;
expect(params && "githubDomain" in params).toBe(false);
});
it("leaves config untouched for a public login with no persisted domain", () => {
const cfg = {} as never;
expect(withGithubCopilotDomainConfig(cfg, "github.com")).toBe(cfg);
});
});