test: remove redundant cases and repeated setup

This commit is contained in:
Amruth Pillai
2026-09-29 23:17:30 +02:00
parent 6cfcdea327
commit 48b27802fd
15 changed files with 106 additions and 322 deletions
@@ -4,21 +4,12 @@ import { fireEvent, render, screen } from "@testing-library/react";
import { beforeAll, describe, expect, it, vi } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { DatesField, readTypedDate } from "./dates-field";
import { DatesField } from "./dates-field";
beforeAll(() => {
i18n.loadAndActivate({ locale: "en-US", messages: {} });
});
describe("readTypedDate", () => {
it.each([
["", null],
["soon", undefined],
])("reads %j as %j", (text, expected) => {
expect(readTypedDate(text, "en-US")).toBe(expected);
});
});
describe("DatesField", () => {
const renderField = (dates: ResumeDates, onChange = vi.fn()) => {
render(
@@ -29,12 +20,20 @@ describe("DatesField", () => {
return onChange;
};
it("saves a typed date and clears the review note", () => {
it("saves and clears readable dates without committing invalid input or the review note", () => {
const onChange = renderField({ start: "2016-06", end: "2018", present: false, raw: "Summer 2016 - 2018" });
expect(screen.getByText(/We read "Summer 2016 - 2018"/)).toBeInTheDocument();
fireEvent.change(screen.getByRole("textbox", { name: "Start" }), { target: { value: "Jul 2016" } });
expect(onChange).toHaveBeenLastCalledWith({ start: "2016-07", end: "2018", present: false });
onChange.mockClear();
fireEvent.change(screen.getByRole("textbox", { name: "Start" }), { target: { value: "" } });
expect(onChange).toHaveBeenCalledWith({ start: null, end: "2018", present: false });
onChange.mockClear();
fireEvent.change(screen.getByRole("textbox", { name: "Start" }), { target: { value: "soon" } });
expect(onChange).not.toHaveBeenCalled();
});
});
@@ -12,7 +12,7 @@ import { cn } from "@reactive-resume/utils/style";
type DateSettings = { locale: string; format?: DateFormat | undefined };
/** What someone typed as a date: a year-month, null when cleared, or undefined when it can't be read. */
export function readTypedDate(text: string, locale: string): YearMonth | null | undefined {
function readTypedDate(text: string, locale: string): YearMonth | null | undefined {
const value = text.trim();
if (!value) return null;
const reading = readSingleDate(value, locale);
+29 -180
View File
@@ -478,174 +478,6 @@ describe("agentService.messages.send", () => {
);
});
it("merges an answered ask-user-question tool result into the existing assistant message", async () => {
const activeThread = buildActiveThread();
const userMessage = {
id: "message-user-1",
userId: "user-1",
threadId: "thread-1",
role: "user",
status: "completed",
sequence: 0,
uiMessage: {
id: "ui-user-1",
role: "user",
parts: [{ type: "text", text: "Change the name" }],
},
};
const unansweredAssistantMessage = {
id: "message-assistant-1",
userId: "user-1",
threadId: "thread-1",
role: "assistant",
status: "completed",
sequence: 1,
uiMessage: {
id: "ui-assistant-1",
role: "assistant",
parts: [
{
type: "tool-ask_user_question",
toolCallId: "call-1",
state: "input-available",
input: {
question: "How broadly should I rename?",
choices: ["Only change the main resume header name"],
},
},
],
},
};
const answeredAssistantMessage = {
...unansweredAssistantMessage,
uiMessage: {
...unansweredAssistantMessage.uiMessage,
parts: [
{
type: "tool-ask_user_question",
toolCallId: "call-1",
state: "output-available",
input: {
question: "How broadly should I rename?",
choices: ["Only change the main resume header name"],
},
output: "Only change the main resume header name",
callProviderMetadata: { openai: { itemId: "fc_duplicate_item" } },
resultProviderMetadata: { openai: { itemId: "fc_duplicate_item" } },
},
],
},
};
const answeredAssistantModelInput = {
...answeredAssistantMessage.uiMessage,
parts: [
{
type: "tool-ask_user_question",
toolCallId: "call-1",
state: "output-available",
input: {
question: "How broadly should I rename?",
choices: ["Only change the main resume header name"],
},
output: "Only change the main resume header name",
},
],
};
const updateSets: unknown[] = [];
dbMock.select
.mockImplementationOnce(() => selectLimitResult([activeThread]))
.mockImplementationOnce(() => selectOrderByResult([userMessage, unansweredAssistantMessage]))
.mockImplementationOnce(() => selectOrderByResult([userMessage, answeredAssistantMessage]));
dbMock.update.mockImplementation(() => ({
set: vi.fn((value) => {
updateSets.push(value);
return { where: vi.fn(async () => undefined) };
}),
}));
claimActiveAgentRunMock.mockResolvedValue(true);
aiProvidersServiceMock.getRunnableById.mockResolvedValue({
id: "provider-1",
provider: "openai",
model: "gpt-5",
apiKey: "secret",
baseURL: null,
});
aiProvidersServiceMock.markUsed.mockResolvedValue(undefined);
const [{ convertToModelMessages, ToolLoopAgent }, { agentStreamLifecycle }] = await Promise.all([
import("ai"),
import("./streams"),
]);
vi.mocked(convertToModelMessages).mockResolvedValue([
{ role: "user", content: [{ type: "text", text: "Change the name" }] },
{
role: "assistant",
content: [{ type: "tool-call", toolCallId: "call-1", toolName: "ask_user_question", input: {} }],
},
{
role: "tool",
content: [
{
type: "tool-result",
toolCallId: "call-1",
toolName: "ask_user_question",
output: "Only change the main resume header name",
},
],
},
] as never);
class MockToolLoopAgent {
stream = vi.fn(async () => ({ toUIMessageStream: vi.fn(() => new ReadableStream()) }));
}
vi.mocked(ToolLoopAgent).mockImplementation(MockToolLoopAgent as never);
vi.mocked(agentStreamLifecycle.create).mockResolvedValue(new ReadableStream());
vi.mocked(streamToEventIterator).mockReturnValue("iterator" as never);
const { agentService } = await import("./service");
await agentService.messages.send({
threadId: "thread-1",
userId: "user-1",
message: {
id: "ui-assistant-1",
role: "assistant",
parts: [
{
type: "tool-ask_user_question",
toolCallId: "call-1",
state: "output-available",
input: {
question: "How broadly should I rename?",
choices: ["Only change the main resume header name"],
},
output: "Only change the main resume header name",
},
],
// biome-ignore lint/suspicious/noExplicitAny: minimal fixture for unit test
} as any,
});
expect(dbMock.insert).not.toHaveBeenCalled();
expect(updateSets).toContainEqual(
expect.objectContaining({
uiMessage: expect.objectContaining({
parts: [
expect.objectContaining({
type: "tool-ask_user_question",
toolCallId: "call-1",
state: "output-available",
output: "Only change the main resume header name",
}),
],
}),
}),
);
expect(convertToModelMessages).toHaveBeenCalledWith([userMessage.uiMessage, answeredAssistantModelInput]);
});
// Regression (defect 8): a question continuation streams into the SAME uiMessage id; onFinish
// must upsert the existing assistant row instead of inserting a duplicate row.
it("continues the existing assistant row on a question continuation instead of inserting a duplicate", async () => {
@@ -673,27 +505,41 @@ describe("agentService.messages.send", () => {
parts: [{ type: "tool-ask_user_question", toolCallId: "call-1", state: "input-available", input: question }],
},
};
const answeredAssistantModelInput = {
...unansweredAssistantMessage.uiMessage,
parts: [
{
type: "tool-ask_user_question",
toolCallId: "call-1",
state: "output-available",
input: question,
output: "Only the header",
},
],
};
const answeredAssistantMessage = {
...unansweredAssistantMessage,
uiMessage: {
...unansweredAssistantMessage.uiMessage,
parts: [
{
type: "tool-ask_user_question",
toolCallId: "call-1",
state: "output-available",
input: question,
output: "Only the header",
},
],
...answeredAssistantModelInput,
parts: answeredAssistantModelInput.parts.map((part) => ({
...part,
callProviderMetadata: { openai: { itemId: "fc_duplicate_item" } },
resultProviderMetadata: { openai: { itemId: "fc_duplicate_item" } },
})),
},
};
const updateSets: unknown[] = [];
dbMock.select
.mockImplementationOnce(() => selectLimitResult([activeThread]))
.mockImplementationOnce(() => selectOrderByResult([userMessage, unansweredAssistantMessage]))
.mockImplementationOnce(() => selectOrderByResult([userMessage, answeredAssistantMessage]));
dbMock.update.mockImplementation(() => ({ set: vi.fn(() => ({ where: vi.fn(async () => undefined) })) }));
dbMock.update.mockImplementation(() => ({
set: vi.fn((value) => {
updateSets.push(value);
return { where: vi.fn(async () => undefined) };
}),
}));
claimActiveAgentRunMock.mockResolvedValue(true);
aiProvidersServiceMock.getRunnableById.mockResolvedValue({
@@ -735,9 +581,12 @@ describe("agentService.messages.send", () => {
threadId: "thread-1",
userId: "user-1",
// biome-ignore lint/suspicious/noExplicitAny: minimal fixture for unit test
message: answeredAssistantMessage.uiMessage as any,
message: answeredAssistantModelInput as any,
});
expect(updateSets).toContainEqual(expect.objectContaining({ uiMessage: answeredAssistantModelInput }));
expect(convertToModelMessages).toHaveBeenCalledWith([userMessage.uiMessage, answeredAssistantModelInput]);
const onFinish = uiStreamOptions?.onFinish as (event: Record<string, unknown>) => Promise<void>;
const continuedMessage = {
...answeredAssistantMessage.uiMessage,
-16
View File
@@ -36,10 +36,6 @@ AWS Solutions Architect Amazon 2021
describe("parseResumeText", () => {
const data = parseResumeText(SAMPLE);
it("always returns schema-valid resume data", () => {
expect(() => resumeDataSchema.parse(data)).not.toThrow();
});
it("reads the contact block", () => {
expect(data.basics).toMatchObject({
name: "Ada Lovelace",
@@ -149,18 +145,6 @@ describe("parseResumeText edge cases", () => {
});
});
describe("parseResumeText review findings", () => {
it("keeps a section whose heading is the first one in the document", () => {
const data = parseResumeText(
"Ada Lovelace\nada@example.com\n\nCAREER HIGHLIGHTS\nShipped the difference engine\nMentored the team\n",
);
expect(data.customSections).toHaveLength(1);
expect(data.customSections[0]).toMatchObject({ title: "CAREER HIGHLIGHTS" });
expect(JSON.stringify(data)).toContain("Shipped the difference engine");
});
});
describe("parseResumeText multi-line entry preambles", () => {
it("keeps an uppercase company followed by a separate role line as one entry", () => {
const data = parseResumeText(
@@ -63,7 +63,9 @@ const finalTextStyle = async (template: Template, text: string, rule = "") => {
const element = createElement(ResumeDocument, { data, template }) as unknown as Parameters<typeof pdf>[0];
const instance = pdf(element);
await expect.poll(() => instance.container.document).not.toBeNull();
return mergedStyle(findText(instance.container.document as HostNode, text));
const node = findText(instance.container.document as HostNode, text);
expect(node).toBeDefined();
return mergedStyle(node);
};
describe("PDF semantic base and reset fidelity", () => {
@@ -23,18 +23,8 @@ type RasterLoadingTask = {
destroy(): Promise<void>;
};
export type RasterizePdfDependencies = {
loadDocument?: (bytes: Uint8Array) => RasterLoadingTask;
};
const loadPdfDocument = (bytes: Uint8Array): RasterLoadingTask =>
getDocument({ data: bytes }) as unknown as RasterLoadingTask;
export async function rasterizePdf(
bytes: Uint8Array,
dependencies: RasterizePdfDependencies = {},
): Promise<readonly RasterizedPdfPage[]> {
const loadingTask = (dependencies.loadDocument ?? loadPdfDocument)(bytes);
export async function rasterizePdf(bytes: Uint8Array): Promise<readonly RasterizedPdfPage[]> {
const loadingTask = getDocument({ data: bytes }) as unknown as RasterLoadingTask;
const pages: RasterizedPdfPage[] = [];
try {
@@ -95,7 +95,7 @@ const renderGengar = async (data: ResumeData, template: Template = "gengar") =>
const page = await pdf.getPage(index);
pages.push((await page.getTextContent()).items.flatMap((item) => ("str" in item ? [item] : [])));
}
return { bytes, pages, raster: await rasterizePdf(bytes.slice()) };
return { bytes, pages };
} finally {
await loading.destroy();
}
@@ -141,7 +141,8 @@ describe("Gengar skill rating placement (#2611)", () => {
const data = gengarFixture({ design: "rectangle" });
const result = await renderGengar(data);
const text = result.pages.flat().map((item) => item.str);
const raster = required(result.raster[0]);
const rasters = await rasterizePdf(result.bytes.slice());
const raster = required(rasters[0]);
const rows = Array.from({ length: raster.height }, (_, y) => {
let pixels = 0;
for (let x = 0; x < raster.width; x++) {
@@ -159,6 +160,6 @@ describe("Gengar skill rating placement (#2611)", () => {
expect(ratingY).toBeLessThan(nameY);
expect(ratingY).toBeGreaterThan(proficiencyY);
expect(text.join(" ")).toContain("Engineering");
expect(result.raster.length).toBe(1);
expect(rasters).toHaveLength(1);
});
});
@@ -1,8 +0,0 @@
import { describe, expect, it } from "vitest";
import { getSectionItemRows } from "./columns";
describe("getSectionItemRows", () => {
it("groups items into rows of N columns", () => {
expect(getSectionItemRows([1, 2, 3, 4, 5], 2)).toEqual([[1, 2], [3, 4], [5]]);
});
});
@@ -68,9 +68,9 @@ async function picturePixels(template: "onyx" | "ditto" | "glalie", borderWidth:
}
describe("picture border visibility (#3017)", () => {
it.each(["onyx"] as const)("draws the border around an opaque picture (%s)", async (template) => {
const plain = await picturePixels(template, 0);
const bordered = await picturePixels(template, 10);
it("draws a border and a soft centered shadow without moving the photo", async () => {
const plain = await picturePixels("onyx", 0);
const bordered = await picturePixels("onyx", 10);
expect(plain.borderPixels).toBe(0);
expect(bordered.borderPixels).toBeGreaterThan(1000);
expect(bordered.imagePixels).toBeGreaterThan(1000);
@@ -78,10 +78,7 @@ describe("picture border visibility (#3017)", () => {
// Rasterized border/image edges can differ by one antialiased pixel.
for (const edge of ["left", "right", "top", "bottom"] as const)
expect(Math.abs(bordered.bounds[edge] - plain.bounds[edge])).toBeLessThanOrEqual(1);
});
it.each(["onyx"] as const)("draws a soft centered shadow without moving the photo (%s)", async (template) => {
const plain = await picturePixels(template, 0);
const shadow = await picturePixels(template, 0, 10);
const shadow = await picturePixels("onyx", 0, 10);
expect(plain.shadowPixels).toBe(0);
expect(shadow.shadowPixels).toBeGreaterThan(100);
expect(shadow.bounds).toEqual(plain.bounds);
@@ -11,7 +11,8 @@ const renderHeading = async (css: string, hideSectionIcons = false) => {
data.metadata.typography.body.fontFamily = "Helvetica";
data.metadata.typography.heading.fontFamily = "Helvetica";
data.metadata.page.hideSectionIcons = hideSectionIcons;
data.metadata.layout.pages = [{ fullWidth: true, main: ["skills"], sidebar: [] }];
data.metadata.layout.pages = [{ fullWidth: true, main: ["skills", "summary"], sidebar: [] }];
data.summary.content = "<p>Summary body</p>";
data.metadata.stylesheet = { mode: "semantic", source: { languageVersion: 1, text: `@version 1; ${css}` } };
data.sections.skills.items = [
{ id: "skill", hidden: false, name: "Skill", proficiency: "", level: 0, keywords: [], icon: "", iconColor: "" },
@@ -53,12 +54,12 @@ const renderHeading = async (css: string, hideSectionIcons = false) => {
};
describe("Semantic section heading colors (#3348)", () => {
it.each([false])("colors heading text with hideSectionIcons=%s", async (hidden) => {
const { text } = await renderHeading("section-heading { color: #1234ef; }", hidden);
it("applies heading color and text styles with section icons visible", async () => {
const { text, sizes } = await renderHeading(
"section-heading { color: #1234ef; } section[type='skills'] section-heading { font-size: 21pt; text-transform: lowercase; }",
);
expect(text).toContainEqual({ value: "heading", fill: "#1234ef" });
expect(text).toContainEqual({ value: "HEADING", fill: "#1234ef" });
});
it.each([false])("gives heading text its text styles with hideSectionIcons=%s", async (hidden) => {
const { sizes } = await renderHeading("section-heading { font-size: 21pt; text-transform: lowercase; }", hidden);
expect(sizes).toContainEqual({ value: "heading", size: 21 });
});
});
@@ -92,6 +92,8 @@ describe("skill rating alignment (#3343)", () => {
expect(rows.map((row) => row.circles)).toEqual([10, 5]);
expect(text).toContain("Keyword11");
expect(text).toContain("Skill 2");
expect(text.indexOf("Skill 0")).toBeLessThan(text.indexOf("Skill 1"));
expect(text.indexOf("Skill 1")).toBeLessThan(text.indexOf("Skill 2"));
});
it("aligns language ratings in multi-column rows with unequal fluency text", async () => {
const {
+2 -5
View File
@@ -67,8 +67,6 @@ export type HarvestOptions = {
operatorBudgetPerPageMs?: number;
onProgress?: (progress: HarvestProgress) => void;
signal?: { aborted: boolean };
/** Injected for tests; defaults to `Date.now`. */
monotonicNow?: () => number;
};
export const HARVEST_DEFAULTS = {
@@ -253,7 +251,6 @@ async function withTimeout<T>(work: Promise<T>, budgetMs: number): Promise<T | n
* short, `operators` stays null and the report says which checks it could not run.
*/
export async function harvestPdfDocument(document: PdfDocumentLike, options: HarvestOptions): Promise<RawExtraction> {
const now = options.monotonicNow ?? Date.now;
const maxPages = options.maxPages ?? HARVEST_DEFAULTS.maxPages;
const totalBudgetMs = options.operatorBudgetMs ?? HARVEST_DEFAULTS.operatorBudgetMs;
const perPageBudgetMs = options.operatorBudgetPerPageMs ?? HARVEST_DEFAULTS.operatorBudgetPerPageMs;
@@ -280,7 +277,7 @@ export async function harvestPdfDocument(document: PdfDocumentLike, options: Har
const fontRefs = new Set<string>();
const fonts = new Map<string, RawFont>();
const operatorStartedAt = now();
const operatorStartedAt = Date.now();
let operatorsAttempted = false;
let operatorsSucceeded = false;
@@ -311,7 +308,7 @@ export async function harvestPdfDocument(document: PdfDocumentLike, options: Har
links.push(...toLinkAnnotations(annotations, pageNumber));
let operators: PageOperatorSummary | null = null;
const elapsed = now() - operatorStartedAt;
const elapsed = Date.now() - operatorStartedAt;
const remaining = Math.min(perPageBudgetMs, totalBudgetMs - elapsed);
if (remaining > 0) {
-18
View File
@@ -4,7 +4,6 @@ import {
baseSectionSchema,
customFieldSchema,
customSectionSchema,
experienceItemSchema,
pageSchema,
parseResumeData,
pictureSchema,
@@ -89,23 +88,6 @@ describe("customFieldSchema", () => {
});
});
describe("experienceItemSchema", () => {
it("defaults roles to [] via .catch when missing", () => {
const result = experienceItemSchema.safeParse({
id: "x",
hidden: false,
company: "Acme",
position: "",
location: "",
period: "",
website: { url: "", label: "", inlineLink: false },
description: "",
});
expect(result.success).toBe(true);
if (result.success) expect(result.data.roles).toEqual([]);
});
});
describe("pageSchema", () => {
it("defaults hideLinkUnderline to false when missing", () => {
const { hideLinkUnderline: _, ...pageWithout } = defaultResumeData.metadata.page;
+11 -20
View File
@@ -2,7 +2,9 @@ import { readFile, writeFile } from "node:fs/promises";
import { createSampleResumeFromDashboard, openDownloadDialog, openSidebarSection } from "../fixtures/resume";
import { expect, test } from "../fixtures/test";
test("exports and imports a resume JSON backup", async ({ authPage: page }, testInfo) => {
test("round-trips a JSON backup and splits a legacy embedded letter on import", async ({
authPage: page,
}, testInfo) => {
await createSampleResumeFromDashboard(page, testInfo);
const sheet = await openDownloadDialog(page);
@@ -14,7 +16,11 @@ test("exports and imports a resume JSON backup", async ({ authPage: page }, test
const downloadPath = testInfo.outputPath(download.suggestedFilename());
await download.saveAs(downloadPath);
const exportedData = JSON.parse(await readFile(downloadPath, "utf-8")) as { basics: { name: string } };
const exportedData = JSON.parse(await readFile(downloadPath, "utf-8")) as {
basics: { name: string };
customSections: unknown[];
metadata: { layout: { pages: { fullWidth: boolean; main: string[]; sidebar: string[] }[] } };
};
// A name only this file carries, so a builder that opens anything but the imported resume fails.
exportedData.basics.name = `Imported ${Date.now()}`;
await writeFile(downloadPath, JSON.stringify(exportedData));
@@ -29,24 +35,9 @@ test("exports and imports a resume JSON backup", async ({ authPage: page }, test
await page.waitForURL(/\/builder\/.+/);
await openSidebarSection(page, "Basics");
await expect(page.getByRole("textbox", { name: "Full name", exact: true })).toHaveValue(exportedData.basics.name);
});
test("imports a resume file that carries a cover letter as a resume and a letter of its own", async ({
authPage: page,
}, testInfo) => {
await createSampleResumeFromDashboard(page, testInfo);
const sheet = await openDownloadDialog(page);
await sheet.getByRole("radio", { name: /^JSON/ }).click();
const downloadPromise = page.waitForEvent("download");
await sheet.getByRole("button", { name: "Download JSON" }).click();
const exported = await downloadPromise;
const data = JSON.parse(await readFile((await exported.path()) as string, "utf-8")) as {
customSections: unknown[];
metadata: { layout: { pages: { fullWidth: boolean; main: string[]; sidebar: string[] }[] } };
};
// A file from an older version, with the letter inside the resume.
data.customSections.push({
exportedData.customSections.push({
id: "old-letter",
type: "cover-letter",
title: "Letter to Globex",
@@ -57,9 +48,9 @@ test("imports a resume file that carries a cover letter as a resume and a letter
startOnNewPage: false,
items: [{ id: "old-letter-item", hidden: false, recipient: "<p>Globex</p>", content: "<p>Dear Globex team,</p>" }],
});
data.metadata.layout.pages.push({ fullWidth: true, main: ["old-letter"], sidebar: [] });
exportedData.metadata.layout.pages.push({ fullWidth: true, main: ["old-letter"], sidebar: [] });
const path = testInfo.outputPath("with-letter.json");
await writeFile(path, JSON.stringify(data));
await writeFile(path, JSON.stringify(exportedData));
await page.goto("/dashboard");
await page.getByRole("button", { name: "New", exact: true }).click();
+31 -34
View File
@@ -1,37 +1,34 @@
import { createHash, randomBytes } from "node:crypto";
import { expect, test } from "../fixtures/test";
for (const accept of [false, true]) {
test(`requires explicit OAuth consent before ${accept ? "allowing" : "denying"} access`, async ({
authPage: page,
baseURL,
}) => {
const origin = new URL(baseURL ?? "http://localhost:3000").origin;
const metadata = await page.request.get("/.well-known/oauth-protected-resource");
expect(metadata.status()).toBe(200);
const advertisedResource = (await metadata.json()).resource;
expect(advertisedResource).toBe(origin);
const resource = `${origin}/mcp`;
const callback = "http://127.0.0.1:33921/callback";
const registration = await page.request.post("/api/auth/oauth2/register", {
headers: { origin },
data: { client_name: "Consent test client", redirect_uris: [callback] },
});
expect(registration.status(), await registration.text()).toBe(201);
const client = await registration.json();
const verifier = randomBytes(32).toString("base64url");
const query = new URLSearchParams({
client_id: client.client_id,
redirect_uri: callback,
response_type: "code",
scope: "openid profile offline_access",
code_challenge: createHash("sha256").update(verifier).digest("base64url"),
code_challenge_method: "S256",
resource,
state: "browser-consent-state",
});
query.append("resource", origin);
await page.route(`${callback}**`, (route) => route.fulfill({ body: "Client callback" }));
test("requires explicit OAuth consent before denying or allowing access", async ({ authPage: page, baseURL }) => {
const origin = new URL(baseURL ?? "http://localhost:3000").origin;
const metadata = await page.request.get("/.well-known/oauth-protected-resource");
expect(metadata.status()).toBe(200);
const advertisedResource = (await metadata.json()).resource;
expect(advertisedResource).toBe(origin);
const resource = `${origin}/mcp`;
const callback = "http://127.0.0.1:33921/callback";
const registration = await page.request.post("/api/auth/oauth2/register", {
headers: { origin },
data: { client_name: "Consent test client", redirect_uris: [callback] },
});
expect(registration.status(), await registration.text()).toBe(201);
const client = await registration.json();
const verifier = randomBytes(32).toString("base64url");
const query = new URLSearchParams({
client_id: client.client_id,
redirect_uri: callback,
response_type: "code",
scope: "openid profile offline_access",
code_challenge: createHash("sha256").update(verifier).digest("base64url"),
code_challenge_method: "S256",
resource,
state: "browser-consent-state",
});
query.append("resource", origin);
await page.route(`${callback}**`, (route) => route.fulfill({ body: "Client callback" }));
for (const accept of [false, true]) {
await page.goto(`/api/auth/oauth2/authorize?${query}`);
await expect(page.getByRole("heading", { name: "Connect an application" })).toBeVisible();
await expect(page.getByText("Consent test client", { exact: true })).toBeVisible();
@@ -49,7 +46,7 @@ for (const accept of [false, true]) {
expect(target.searchParams.has("code")).toBe(false);
const after = await page.request.get(`${origin}/api/auth/oauth2/get-consents`);
expect(await after.json()).toEqual([]);
return;
continue;
}
expect(target.searchParams.get("code")).toBeTruthy();
const token = await page.request.post(`${origin}/api/auth/oauth2/token`, {
@@ -89,5 +86,5 @@ for (const accept of [false, true]) {
data: initializePayload,
});
expect(wrongAudience.status()).toBe(401);
});
}
}
});