Skip to content
Merged
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
115 changes: 115 additions & 0 deletions lib/sandbox/__tests__/getSandboxesHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getSandboxesHandler } from "../getSandboxesHandler";
import { validateGetSandboxesRequest } from "../validateGetSandboxesRequest";
import { selectAccountSandboxes } from "@/lib/supabase/account_sandboxes/selectAccountSandboxes";
import { getSandboxStatus } from "../getSandboxStatus";
import { selectAccountSnapshots } from "@/lib/supabase/account_snapshots/selectAccountSnapshots";

vi.mock("../validateGetSandboxesRequest", () => ({
validateGetSandboxesRequest: vi.fn(),
Expand All @@ -19,6 +20,10 @@ vi.mock("../getSandboxStatus", () => ({
getSandboxStatus: vi.fn(),
}));

vi.mock("@/lib/supabase/account_snapshots/selectAccountSnapshots", () => ({
selectAccountSnapshots: vi.fn(),
}));

/**
* Creates a mock NextRequest for testing.
*
Expand All @@ -34,6 +39,8 @@ function createMockRequest(): NextRequest {
describe("getSandboxesHandler", () => {
beforeEach(() => {
vi.clearAllMocks();
// Default mock for selectAccountSnapshots - no snapshot exists
vi.mocked(selectAccountSnapshots).mockResolvedValue([]);
});

it("returns error response when validation fails", async () => {
Expand Down Expand Up @@ -61,6 +68,8 @@ describe("getSandboxesHandler", () => {
expect(json).toEqual({
status: "success",
sandboxes: [],
snapshot_id: null,
github_repo: null,
});
});

Expand Down Expand Up @@ -98,6 +107,8 @@ describe("getSandboxesHandler", () => {
createdAt: "2024-01-01T00:00:00.000Z",
},
],
snapshot_id: null,
github_repo: null,
});
});

Expand Down Expand Up @@ -238,4 +249,108 @@ describe("getSandboxesHandler", () => {
const minEndIndex = Math.min(...endIndices);
expect(maxStartIndex).toBeLessThan(minEndIndex);
});

describe("snapshot_id and github_repo fields", () => {
it("returns snapshot_id and github_repo when account has a snapshot", async () => {
vi.mocked(validateGetSandboxesRequest).mockResolvedValue({
accountIds: ["acc_123"],
});
vi.mocked(selectAccountSandboxes).mockResolvedValue([]);
vi.mocked(selectAccountSnapshots).mockResolvedValue([
{
account_id: "acc_123",
snapshot_id: "snap_abc123",
github_repo: "https://github.com/user/repo",
created_at: "2024-01-01T00:00:00.000Z",
expires_at: "2024-01-08T00:00:00.000Z",
},
]);

const request = createMockRequest();
const response = await getSandboxesHandler(request);

expect(response.status).toBe(200);
const json = await response.json();
expect(json.snapshot_id).toBe("snap_abc123");
expect(json.github_repo).toBe("https://github.com/user/repo");
});

it("returns null for snapshot_id and github_repo when account has no snapshot", async () => {
vi.mocked(validateGetSandboxesRequest).mockResolvedValue({
accountIds: ["acc_123"],
});
vi.mocked(selectAccountSandboxes).mockResolvedValue([]);
vi.mocked(selectAccountSnapshots).mockResolvedValue([]);

const request = createMockRequest();
const response = await getSandboxesHandler(request);

expect(response.status).toBe(200);
const json = await response.json();
expect(json.snapshot_id).toBeNull();
expect(json.github_repo).toBeNull();
});

it("returns null github_repo when snapshot exists but has no github_repo", async () => {
vi.mocked(validateGetSandboxesRequest).mockResolvedValue({
accountIds: ["acc_123"],
});
vi.mocked(selectAccountSandboxes).mockResolvedValue([]);
vi.mocked(selectAccountSnapshots).mockResolvedValue([
{
account_id: "acc_123",
snapshot_id: "snap_abc123",
github_repo: null,
created_at: "2024-01-01T00:00:00.000Z",
expires_at: "2024-01-08T00:00:00.000Z",
},
]);

const request = createMockRequest();
const response = await getSandboxesHandler(request);

expect(response.status).toBe(200);
const json = await response.json();
expect(json.snapshot_id).toBe("snap_abc123");
expect(json.github_repo).toBeNull();
});

it("returns snapshot info for org keys using orgId as accountId", async () => {
vi.mocked(validateGetSandboxesRequest).mockResolvedValue({
orgId: "org_123",
});
vi.mocked(selectAccountSandboxes).mockResolvedValue([]);
vi.mocked(selectAccountSnapshots).mockResolvedValue([
{
account_id: "org_123",
snapshot_id: "snap_org_abc",
github_repo: "https://github.com/org/repo",
created_at: "2024-01-01T00:00:00.000Z",
expires_at: "2024-01-08T00:00:00.000Z",
},
]);

const request = createMockRequest();
const response = await getSandboxesHandler(request);

expect(response.status).toBe(200);
const json = await response.json();
expect(json.snapshot_id).toBe("snap_org_abc");
expect(json.github_repo).toBe("https://github.com/org/repo");
expect(selectAccountSnapshots).toHaveBeenCalledWith("org_123");
});

it("calls selectAccountSnapshots with the account ID", async () => {
vi.mocked(validateGetSandboxesRequest).mockResolvedValue({
accountIds: ["acc_123"],
});
vi.mocked(selectAccountSandboxes).mockResolvedValue([]);
vi.mocked(selectAccountSnapshots).mockResolvedValue([]);

const request = createMockRequest();
await getSandboxesHandler(request);

expect(selectAccountSnapshots).toHaveBeenCalledWith("acc_123");
});
});
});
20 changes: 17 additions & 3 deletions lib/sandbox/getSandboxesHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { validateGetSandboxesRequest } from "./validateGetSandboxesRequest";
import { selectAccountSandboxes } from "@/lib/supabase/account_sandboxes/selectAccountSandboxes";
import { selectAccountSnapshots } from "@/lib/supabase/account_snapshots/selectAccountSnapshots";
import { getSandboxStatus } from "./getSandboxStatus";
import type { SandboxCreatedResponse } from "./createSandbox";

Expand All @@ -16,16 +17,23 @@ import type { SandboxCreatedResponse } from "./createSandbox";
* - sandbox_id: Filter to a specific sandbox (must belong to account/org)
*
* @param request - The request object.
* @returns A NextResponse with array of sandbox statuses.
* @returns A NextResponse with array of sandbox statuses, plus snapshot_id and github_repo.
*/
export async function getSandboxesHandler(request: NextRequest): Promise<NextResponse> {
const validated = await validateGetSandboxesRequest(request);
if (validated instanceof NextResponse) {
return validated;
}

// Get sandbox records from database
const accountSandboxes = await selectAccountSandboxes(validated);
// Determine account ID for snapshot lookup
const snapshotAccountId =
validated.accountIds?.length === 1 ? validated.accountIds[0] : validated.orgId;

// Fetch sandbox records and snapshot info in parallel
const [accountSandboxes, snapshots] = await Promise.all([
selectAccountSandboxes(validated),
snapshotAccountId ? selectAccountSnapshots(snapshotAccountId) : Promise.resolve([]),
]);

// Fetch live status for each sandbox from Vercel API in parallel
const statusResults = await Promise.all(
Expand All @@ -37,10 +45,16 @@ export async function getSandboxesHandler(request: NextRequest): Promise<NextRes
(status): status is SandboxCreatedResponse => status !== null,
);

// Extract snapshot info
const snapshot_id = snapshots[0]?.snapshot_id ?? null;
const github_repo = snapshots[0]?.github_repo ?? null;

return NextResponse.json(
{
status: "success",
sandboxes,
snapshot_id,
github_repo,
},
{
status: 200,
Expand Down
3 changes: 3 additions & 0 deletions types/database.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,18 +305,21 @@ export type Database = {
account_id: string
created_at: string | null
expires_at: string
github_repo: string | null
snapshot_id: string
}
Insert: {
account_id: string
created_at?: string | null
expires_at: string
github_repo?: string | null
snapshot_id: string
}
Update: {
account_id?: string
created_at?: string | null
expires_at?: string
github_repo?: string | null
snapshot_id?: string
}
Relationships: [
Expand Down