forked from EduCraft/curriculum-project-hub
113 lines
3.7 KiB
TypeScript
113 lines
3.7 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
buildAuthorizeUrl,
|
|
exchangeCodeForUser,
|
|
FeishuOAuthError,
|
|
type FeishuOAuthConfig,
|
|
} from "../../src/admin/auth/feishuOAuth.js";
|
|
|
|
describe("buildAuthorizeUrl", () => {
|
|
it("includes client_id, redirect, state, and scope", () => {
|
|
const url = buildAuthorizeUrl(
|
|
{
|
|
appId: "cli_test",
|
|
appSecret: "secret",
|
|
redirectUri: "http://localhost:8788/auth/feishu/callback",
|
|
scope: "contact:user.base:readonly",
|
|
},
|
|
"state-token",
|
|
);
|
|
const parsed = new URL(url);
|
|
expect(parsed.origin + parsed.pathname).toBe(
|
|
"https://accounts.feishu.cn/open-apis/authen/v1/authorize",
|
|
);
|
|
expect(parsed.searchParams.get("client_id")).toBe("cli_test");
|
|
expect(parsed.searchParams.get("response_type")).toBe("code");
|
|
expect(parsed.searchParams.get("redirect_uri")).toBe(
|
|
"http://localhost:8788/auth/feishu/callback",
|
|
);
|
|
expect(parsed.searchParams.get("state")).toBe("state-token");
|
|
expect(parsed.searchParams.get("scope")).toBe("contact:user.base:readonly");
|
|
});
|
|
});
|
|
|
|
describe("exchangeCodeForUser", () => {
|
|
it("exchanges code then loads user_info", async () => {
|
|
const fetchImpl = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
const url = String(input);
|
|
if (url.includes("/oauth/token")) {
|
|
expect(init?.method).toBe("POST");
|
|
const body = JSON.parse(String(init?.body)) as { code: string };
|
|
expect(body.code).toBe("auth-code");
|
|
return new Response(
|
|
JSON.stringify({ code: 0, access_token: "u-token", token_type: "Bearer", expires_in: 7200 }),
|
|
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
}
|
|
if (url.includes("/user_info")) {
|
|
expect((init?.headers as Record<string, string>).Authorization).toBe("Bearer u-token");
|
|
return new Response(
|
|
JSON.stringify({
|
|
code: 0,
|
|
data: { open_id: "ou_alice", name: "Alice", avatar_url: "https://img/a.png" },
|
|
}),
|
|
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
}
|
|
throw new Error(`unexpected url ${url}`);
|
|
});
|
|
|
|
const config: FeishuOAuthConfig = {
|
|
appId: "cli_test",
|
|
appSecret: "secret",
|
|
redirectUri: "http://localhost:8788/auth/feishu/callback",
|
|
scope: "contact:user.base:readonly",
|
|
fetchImpl: fetchImpl as unknown as typeof fetch,
|
|
};
|
|
|
|
const user = await exchangeCodeForUser(config, "auth-code");
|
|
expect(user).toEqual({
|
|
openId: "ou_alice",
|
|
displayName: "Alice",
|
|
avatarUrl: "https://img/a.png",
|
|
});
|
|
expect(fetchImpl).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("throws on token exchange failure", async () => {
|
|
const fetchImpl = vi.fn(async () =>
|
|
new Response(JSON.stringify({
|
|
code: 20003,
|
|
error: "invalid_grant",
|
|
error_description: "echoed app secret s and authorization code bad",
|
|
}), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
}),
|
|
);
|
|
let failure: unknown;
|
|
try {
|
|
await exchangeCodeForUser(
|
|
{
|
|
appId: "cli",
|
|
appSecret: "s",
|
|
redirectUri: "http://localhost/cb",
|
|
scope: "",
|
|
fetchImpl: fetchImpl as unknown as typeof fetch,
|
|
},
|
|
"bad",
|
|
);
|
|
} catch (error) {
|
|
failure = error;
|
|
}
|
|
expect(failure).toBeInstanceOf(FeishuOAuthError);
|
|
expect(failure).toMatchObject({
|
|
code: "feishu_oauth_rejected",
|
|
category: "provider_rejection",
|
|
upstreamStatus: 200,
|
|
});
|
|
expect(String(failure)).not.toContain("secret s");
|
|
expect(String(failure)).not.toContain("authorization code bad");
|
|
});
|
|
});
|