From 496b7781c803f60a2108fcd2673acee82777c061 Mon Sep 17 00:00:00 2001 From: CTO Agent Date: Tue, 7 Apr 2026 14:09:27 +0000 Subject: [PATCH] feat: add account_id query param to GET /api/sandboxes/file Allows the /files page to show files for an overridden account when using the email query param override flow. The AccountOverrideProvider resolves the email to an accountId, and the chat frontend passes it as account_id to this endpoint. Co-Authored-By: Paperclip --- .../validateGetSandboxesFileRequest.test.ts | 134 ++++++++++++++++++ .../validateGetSandboxesFileRequest.ts | 6 +- 2 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 lib/sandbox/__tests__/validateGetSandboxesFileRequest.test.ts diff --git a/lib/sandbox/__tests__/validateGetSandboxesFileRequest.test.ts b/lib/sandbox/__tests__/validateGetSandboxesFileRequest.test.ts new file mode 100644 index 00000000..c3deda3d --- /dev/null +++ b/lib/sandbox/__tests__/validateGetSandboxesFileRequest.test.ts @@ -0,0 +1,134 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; + +import { validateGetSandboxesFileRequest } from "../validateGetSandboxesFileRequest"; +import { validateAuthContext } from "@/lib/auth/validateAuthContext"; +import { buildGetSandboxesParams } from "../buildGetSandboxesParams"; + +vi.mock("@/lib/auth/validateAuthContext", () => ({ + validateAuthContext: vi.fn(), +})); + +vi.mock("../buildGetSandboxesParams", () => ({ + buildGetSandboxesParams: vi.fn(), +})); + +vi.mock("@/lib/networking/getCorsHeaders", () => ({ + getCorsHeaders: vi.fn(() => ({ "Access-Control-Allow-Origin": "*" })), +})); + +/** + * Creates a mock NextRequest with query parameters. + * + * @param queryParams - Key-value pairs to set as URL search parameters + * @returns A mock NextRequest object + */ +function createMockRequest(queryParams: Record = {}): NextRequest { + const url = new URL("http://localhost:3000/api/sandboxes/file"); + Object.entries(queryParams).forEach(([key, value]) => { + url.searchParams.set(key, value); + }); + return { + url: url.toString(), + headers: new Headers({ "x-api-key": "test-key" }), + } as unknown as NextRequest; +} + +describe("validateGetSandboxesFileRequest", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns error when path is missing", async () => { + const request = createMockRequest(); + const result = await validateGetSandboxesFileRequest(request); + + expect(result).toBeInstanceOf(NextResponse); + expect((result as NextResponse).status).toBe(400); + }); + + it("returns validated params for basic request", async () => { + vi.mocked(validateAuthContext).mockResolvedValue({ + accountId: "acc_123", + orgId: null, + authToken: "token", + }); + vi.mocked(buildGetSandboxesParams).mockResolvedValue({ + params: { accountIds: ["acc_123"], sandboxId: undefined }, + error: null, + }); + + const request = createMockRequest({ path: "src/index.ts" }); + const result = await validateGetSandboxesFileRequest(request); + + expect(result).not.toBeInstanceOf(NextResponse); + expect(result).toEqual({ + accountIds: ["acc_123"], + orgId: undefined, + path: "src/index.ts", + }); + }); + + describe("account_id query parameter", () => { + it("passes account_id as target_account_id to buildGetSandboxesParams", async () => { + vi.mocked(validateAuthContext).mockResolvedValue({ + accountId: "acc_123", + orgId: null, + authToken: "token", + }); + vi.mocked(buildGetSandboxesParams).mockResolvedValue({ + params: { accountIds: ["a1b2c3d4-e5f6-7890-abcd-ef1234567890"], sandboxId: undefined }, + error: null, + }); + + const request = createMockRequest({ + path: "src/index.ts", + account_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + }); + const result = await validateGetSandboxesFileRequest(request); + + expect(buildGetSandboxesParams).toHaveBeenCalledWith({ + account_id: "acc_123", + target_account_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + }); + expect(result).toEqual({ + accountIds: ["a1b2c3d4-e5f6-7890-abcd-ef1234567890"], + orgId: undefined, + path: "src/index.ts", + }); + }); + + it("returns 403 when access denied to target account", async () => { + vi.mocked(validateAuthContext).mockResolvedValue({ + accountId: "acc_123", + orgId: null, + authToken: "token", + }); + vi.mocked(buildGetSandboxesParams).mockResolvedValue({ + params: null, + error: "Access denied to specified account_id", + }); + + const request = createMockRequest({ + path: "src/index.ts", + account_id: "b2c3d4e5-f6a7-8901-bcde-f12345678901", + }); + const result = await validateGetSandboxesFileRequest(request); + + expect(result).toBeInstanceOf(NextResponse); + expect((result as NextResponse).status).toBe(403); + }); + + it("returns 400 for invalid account_id format", async () => { + const request = createMockRequest({ + path: "src/index.ts", + account_id: "not-a-uuid", + }); + const result = await validateGetSandboxesFileRequest(request); + + expect(result).toBeInstanceOf(NextResponse); + expect((result as NextResponse).status).toBe(400); + }); + }); +}); diff --git a/lib/sandbox/validateGetSandboxesFileRequest.ts b/lib/sandbox/validateGetSandboxesFileRequest.ts index bdda238b..8cef82d7 100644 --- a/lib/sandbox/validateGetSandboxesFileRequest.ts +++ b/lib/sandbox/validateGetSandboxesFileRequest.ts @@ -7,6 +7,7 @@ import { z } from "zod"; const getSandboxesFileQuerySchema = z.object({ path: z.string({ message: "path is required" }).min(1, "path cannot be empty"), + account_id: z.string().uuid("account_id must be a valid UUID").optional(), }); export interface ValidatedGetSandboxesFileParams { @@ -21,6 +22,7 @@ export interface ValidatedGetSandboxesFileParams { * * Query parameters: * - path: The file path within the repository (required) + * - account_id: Filter to a specific account (validated against org membership) * * @param request - The NextRequest object * @returns A NextResponse with an error if validation fails, or validated params @@ -31,6 +33,7 @@ export async function validateGetSandboxesFileRequest( const { searchParams } = new URL(request.url); const queryParams = { path: searchParams.get("path") ?? undefined, + account_id: searchParams.get("account_id") ?? undefined, }; const queryResult = getSandboxesFileQuerySchema.safeParse(queryParams); @@ -45,7 +48,7 @@ export async function validateGetSandboxesFileRequest( ); } - const { path } = queryResult.data; + const { path, account_id: targetAccountId } = queryResult.data; const authResult = await validateAuthContext(request); if (authResult instanceof NextResponse) { @@ -56,6 +59,7 @@ export async function validateGetSandboxesFileRequest( const { params, error } = await buildGetSandboxesParams({ account_id: accountId, + target_account_id: targetAccountId, }); if (error) {