| @@ -12,6 +12,8 @@ import type { Identity, Target as TargetShape } from "../src/target"; · 11 unmodified lines |
| 12 | 12 | import type { BrowserSurface } from "../src/surfaces/browser"; |
| 13 | 13 | |
| 14 | 14 | const textEncoder = new TextEncoder(); |
| 15 | +const FIXTURE_GIT_TOKEN = "fixture-token"; |
| 16 | +const EXPECTED_GIT_AUTHORIZATION = `Basic ${btoa(`git:${FIXTURE_GIT_TOKEN}`)}`; |
| 15 | 17 | |
| 16 | 18 | const concat = (parts: readonly Uint8Array[]): Uint8Array => { |
| 17 | 19 | const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0)); |
| @@ -72,6 +74,18 @@ const fixtureGitServer = Effect.acquireRelease( · 54 unmodified lines |
| 72 | 74 | let current = { sha: fixture.sha1, pack: new Uint8Array(fixture.pack1) }; |
| 73 | 75 | let packRequests = 0; |
| 74 | 76 | const server = createServer((request: IncomingMessage, response: ServerResponse) => { |
| 77 | + if ( |
| 78 | + (request.url === "/repo.git/info/refs?service=git-upload-pack" || |
| 79 | + request.url === "/repo.git/git-upload-pack") && |
| 80 | + request.headers.authorization !== EXPECTED_GIT_AUTHORIZATION |
| 81 | + ) { |
| 82 | + response.writeHead(401, { |
| 83 | + "content-type": "text/plain", |
| 84 | + "www-authenticate": 'Basic realm="custom-tools-fixture"', |
| 85 | + }); |
| 86 | + response.end("authentication required"); |
| 87 | + return; |
| 88 | + } |
| 75 | 89 | if (request.url === "/repo.git/info/refs?service=git-upload-pack") { |
| 76 | 90 | response.writeHead(200, { |
| 77 | 91 | "content-type": "application/x-git-upload-pack-advertisement", |
| @@ -208,7 +222,7 @@ const addSourceThroughConsole = (input: { · 130 unmodified lines |
| 208 | 222 | }); |
| 209 | 223 | |
| 210 | 224 | await step("Sync the custom tools source", async () => { |
| 211 | | − await page.locator('input[type="password"]').fill("fixture-token"); |
| 225 | + await page.locator('input[type="password"]').fill(FIXTURE_GIT_TOKEN); |
| 212 | 226 | await page.getByRole("button", { name: "Sync source" }).click(); |
| 213 | 227 | await page.waitForURL(/\/integrations\/repo(?:\?|$)/, { timeout: 90_000 }); |
| 214 | 228 | await page.getByLabel("Source").getByText("2 tools").waitFor({ timeout: 90_000 }); |
| @@ -221,6 +235,47 @@ const addSourceThroughConsole = (input: { · 6 unmodified lines |
| 221 | 235 | }); |
| 222 | 236 | }); |
| 223 | 237 | |
| 238 | +const assertBadTokenFailureThroughConsole = (input: { |
| 239 | + readonly target: TargetShape; |
| 240 | + readonly browser: BrowserSurface; |
| 241 | + readonly identity: Identity; |
| 242 | + readonly sourceUrl: string; |
| 243 | +}) => |
| 244 | + input.browser.session(input.identity, async ({ page, step }) => { |
| 245 | + await step("Try to sync the custom tools source with a bad token", async () => { |
| 246 | + await page.goto(new URL("/integrations", input.target.baseUrl).toString(), { |
| 247 | + waitUntil: "networkidle", |
| 248 | + }); |
| 249 | + await page.getByRole("button", { name: "Connect", exact: true }).click(); |
| 250 | + const dialog = page.getByRole("dialog"); |
| 251 | + await dialog.getByRole("textbox").fill(input.sourceUrl); |
| 252 | + await dialog.getByRole("button", { name: "Detect" }).click(); |
| 253 | + await page.getByRole("heading", { name: "Add custom tools" }).waitFor(); |
| 254 | + await page.locator('input[type="password"]').fill("wrong-token"); |
| 255 | + const syncResponse = page.waitForResponse( |
| 256 | + (response) => |
| 257 | + response.url().includes("/api/apps/sources/repo/sync") && response.status() === 200, |
| 258 | + { timeout: 90_000 }, |
| 259 | + ); |
| 260 | + await page.getByRole("button", { name: "Sync source" }).click(); |
| 261 | + await syncResponse; |
| 262 | + }); |
| 263 | + |
| 264 | + await step("See the failed sync on the source panel", async () => { |
| 265 | + await page.goto(new URL("/integrations/repo?tab=source", input.target.baseUrl).toString(), { |
| 266 | + waitUntil: "networkidle", |
| 267 | + }); |
| 268 | + await page.getByText("Last sync failed").waitFor({ timeout: 90_000 }); |
| 269 | + await page.getByText("source: info/refs 401").waitFor({ timeout: 90_000 }); |
| 270 | + }); |
| 271 | + |
| 272 | + await step("Remove the failed source before retrying with the good token", async () => { |
| 273 | + await page.getByRole("button", { name: "Remove" }).click(); |
| 274 | + await page.getByRole("button", { name: "Remove source" }).click(); |
| 275 | + await page.waitForURL(/\/integrations(?:\?|$)/, { timeout: 90_000 }); |
| 276 | + }); |
| 277 | + }); |
| 278 | + |
| 224 | 279 | const syncSourceInConsole = (input: { |
| 225 | 280 | readonly target: TargetShape; |
| 226 | 281 | readonly browser: BrowserSurface; |
| @@ -279,6 +334,20 @@ scenario( · 52 unmodified lines |
| 279 | 334 | expect(unauthorized.status, "sync requires authentication").toBe(401); |
| 280 | 335 | yield* Effect.promise(() => unauthorized.text()); |
| 281 | 336 | |
| 337 | + const missingGitAuth = yield* Effect.promise(() => |
| 338 | + fetch(`${git.url}/info/refs?service=git-upload-pack`), |
| 339 | + ); |
| 340 | + expect(missingGitAuth.status, "fixture git server requires auth").toBe(401); |
| 341 | + yield* Effect.promise(() => missingGitAuth.text()); |
| 342 | + |
| 343 | + yield* assertBadTokenFailureThroughConsole({ |
| 344 | + target, |
| 345 | + browser, |
| 346 | + identity, |
| 347 | + sourceUrl: git.url, |
| 348 | + }); |
| 349 | + expect(git.packRequests(), "bad token fails before fetching a pack").toBe(0); |
| 350 | + |
| 282 | 351 | yield* addSourceThroughConsole({ target, browser, identity, sourceUrl: git.url }); |
| 283 | 352 | |
| 284 | 353 | const sources = yield* Effect.promise(() => |