Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions lib/sandbox/__tests__/validateGetSandboxesFileRequest.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> = {}): 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);
});
});
});
6 changes: 5 additions & 1 deletion lib/sandbox/validateGetSandboxesFileRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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);
Expand All @@ -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) {
Expand All @@ -56,6 +59,7 @@ export async function validateGetSandboxesFileRequest(

const { params, error } = await buildGetSandboxesParams({
account_id: accountId,
target_account_id: targetAccountId,
});

if (error) {
Expand Down
Loading