From 6604154e3f98b7c738363b7632c69626efc2359c Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 1 Sep 2026 01:39:55 +0900 Subject: [PATCH] test: cover repository access loader failures --- web/workspace/deno.json | 2 +- .../test/repository-access/loader.test.ts | 123 +++++++++++++++--- 2 files changed, 103 insertions(+), 22 deletions(-) diff --git a/web/workspace/deno.json b/web/workspace/deno.json index 2d157d72..92bc60e2 100644 --- a/web/workspace/deno.json +++ b/web/workspace/deno.json @@ -6,7 +6,7 @@ "dev": "deno run -A npm:vite@7.2.7 dev", "dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787", "check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json", - "test": "deno test --allow-read=src,test --allow-env=LOG,VSCODE_TEXTMATE_DEBUG src/lib/workspace/auth/model.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-launch.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts", + "test": "deno test --allow-read=src,test --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV src/lib/workspace/auth/model.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-launch.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts", "build": "deno run -A npm:vite@7.2.7 build", "preview": "deno run -A npm:vite@7.2.7 preview" }, diff --git a/web/workspace/test/repository-access/loader.test.ts b/web/workspace/test/repository-access/loader.test.ts index 7330531d..21b46be3 100644 --- a/web/workspace/test/repository-access/loader.test.ts +++ b/web/workspace/test/repository-access/loader.test.ts @@ -1,33 +1,114 @@ import { loadRepositoryAccessJson } from "../../src/lib/workspace/api/repository-access-loader.ts"; +import { RepositoryAccessSchemaError } from "../../src/lib/workspace/api/repository-access.ts"; -Deno.test("Repository Access loader maps missing permission to a bounded unavailable error", async () => { - let requests = 0; +type HttpFailure = { status?: number; body?: { message?: string } }; + +async function captureHttpFailure( + run: () => Promise, + expectedStatus: number, + expectedMessage: string, +): Promise { try { - await loadRepositoryAccessJson( - () => { - requests += 1; - return Promise.resolve(new Response(null, { status: 403 })); - }, - "/api/w/workspace-1/settings/repository-access/credentials", - (value) => value, - ); + await run(); } catch (error) { - const failure = error as { status?: number; body?: { message?: string } }; - if (failure.status !== 403) { - throw new Error(`expected bounded 403, got ${String(failure.status)}`); - } - if ( - failure.body?.message !== - "Repository Access is unavailable for this account." - ) { + const failure = error as HttpFailure; + if (failure.status !== expectedStatus) { throw new Error( - `unexpected permission error: ${JSON.stringify(failure.body)}`, + `expected bounded ${expectedStatus}, got ${String(failure.status)}`, ); } + if (failure.body?.message !== expectedMessage) { + throw new Error( + `unexpected bounded error: ${JSON.stringify(failure.body)}`, + ); + } + return failure; + } + throw new Error(`expected bounded ${expectedStatus} error`); +} + +for (const status of [401, 403]) { + Deno.test(`Repository Access loader maps ${status} to bounded permission unavailable`, async () => { + let requests = 0; + await captureHttpFailure( + () => + loadRepositoryAccessJson( + () => { + requests += 1; + return Promise.resolve(new Response(null, { status })); + }, + "/api/w/workspace-1/settings/repository-access", + (value) => value, + ), + 403, + "Repository Access is unavailable for this account.", + ); if (requests !== 1) { throw new Error(`expected one bounded request, got ${requests}`); } - return; + }); +} + +Deno.test("Repository Access loader maps invalid JSON to safe bounded 502", async () => { + const upstreamSecret = "private-key-must-not-leak"; + const failure = await captureHttpFailure( + () => + loadRepositoryAccessJson( + () => + Promise.resolve( + new Response(upstreamSecret, { + status: 200, + headers: { "content-type": "application/json" }, + }), + ), + "/api/w/workspace-1/settings/repository-access/credentials", + (value) => value, + ), + 502, + "Repository Access returned an invalid JSON response.", + ); + if (JSON.stringify(failure.body).includes(upstreamSecret)) { + throw new Error("invalid JSON error exposed upstream response content"); + } +}); + +Deno.test("Repository Access loader maps schema mismatch to explicit bounded 502", async () => { + const failure = await captureHttpFailure( + () => + loadRepositoryAccessJson( + () => Promise.resolve(Response.json({ stale: true })), + "/api/w/workspace-1/settings/repository-access/credentials", + () => { + throw new RepositoryAccessSchemaError("credentials", "an array"); + }, + ), + 502, + "Repository Access response schema mismatch at credentials: expected an array", + ); + if (!failure.body?.message?.includes("credentials")) { + throw new Error("schema mismatch error omitted the failing response path"); + } +}); + +Deno.test("Repository Access loader never exposes failed upstream response bodies", async () => { + const upstreamSecret = "secret-ref-must-not-leak"; + const failure = await captureHttpFailure( + () => + loadRepositoryAccessJson( + () => + Promise.resolve( + Response.json( + { message: upstreamSecret, secret_ref: upstreamSecret }, + { status: 500 }, + ), + ), + "/api/w/workspace-1/settings/repository-access/host-trusts", + (value) => value, + ), + 502, + "Repository Access request failed with status 500.", + ); + if (JSON.stringify(failure.body).includes(upstreamSecret)) { + throw new Error("bounded upstream error exposed response content"); } - throw new Error("expected Repository Access permission error"); });