diff --git a/app/api/tasks/runs/route.ts b/app/api/tasks/runs/route.ts index b9c876022..0307141aa 100644 --- a/app/api/tasks/runs/route.ts +++ b/app/api/tasks/runs/route.ts @@ -17,17 +17,18 @@ export async function OPTIONS() { /** * GET /api/tasks/runs * - * Retrieves the status of a Trigger.dev task run. - * Returns one of three possible statuses: - * - pending: Task is still running - * - complete: Task completed successfully with data - * - failed: Task failed with error message + * Returns task runs for the authenticated account. + * When `runId` is provided, returns `{ status: "success", runs: [run] }` or 404 if missing. + * When `runId` is omitted, returns recent runs for account scope (default authenticated account, + * or `account_id` override when authorized). * * Query parameters: - * - runId (required): The unique identifier of the task run + * - runId (optional): Retrieve one specific run + * - account_id (optional): Account scope override (UUID, when authorized) + * - limit (optional): Max runs for list mode (default 20, max 100) * * @param request - The request object containing query parameters. - * @returns A NextResponse with task run status. + * @returns A NextResponse with task run data. */ export async function GET(request: NextRequest) { return getTaskRunHandler(request); diff --git a/lib/tasks/__tests__/validateGetTaskRunQuery.test.ts b/lib/tasks/__tests__/validateGetTaskRunQuery.test.ts index f7126175d..2db4db94f 100644 --- a/lib/tasks/__tests__/validateGetTaskRunQuery.test.ts +++ b/lib/tasks/__tests__/validateGetTaskRunQuery.test.ts @@ -33,6 +33,10 @@ function createMockRequest(url: string): NextRequest { } as unknown as NextRequest; } +const ACCOUNT_ID = "123e4567-e89b-12d3-a456-426614174000"; +const OTHER_ACCOUNT_ID = "223e4567-e89b-12d3-a456-426614174000"; +const MEMBER_ACCOUNT_ID = "323e4567-e89b-12d3-a456-426614174000"; + describe("validateGetTaskRunQuery", () => { beforeEach(() => { vi.clearAllMocks(); @@ -134,13 +138,13 @@ describe("validateGetTaskRunQuery", () => { vi.mocked(checkIsAdmin).mockResolvedValue(true); const request = createMockRequest( - "http://localhost:3000/api/tasks/runs?account_id=other_acc", + `http://localhost:3000/api/tasks/runs?account_id=${OTHER_ACCOUNT_ID}`, ); const result = await validateGetTaskRunQuery(request); expect(result).not.toBeInstanceOf(NextResponse); - expect(result).toEqual({ mode: "list", accountId: "other_acc", limit: 20 }); + expect(result).toEqual({ mode: "list", accountId: OTHER_ACCOUNT_ID, limit: 20 }); expect(checkIsAdmin).toHaveBeenCalledWith("admin_acc"); expect(validateAccountIdOverride).not.toHaveBeenCalled(); }); @@ -152,19 +156,19 @@ describe("validateGetTaskRunQuery", () => { authToken: "api-key", }); vi.mocked(checkIsAdmin).mockResolvedValue(false); - vi.mocked(validateAccountIdOverride).mockResolvedValue({ accountId: "member_acc" }); + vi.mocked(validateAccountIdOverride).mockResolvedValue({ accountId: MEMBER_ACCOUNT_ID }); const request = createMockRequest( - "http://localhost:3000/api/tasks/runs?account_id=member_acc", + `http://localhost:3000/api/tasks/runs?account_id=${MEMBER_ACCOUNT_ID}`, ); const result = await validateGetTaskRunQuery(request); expect(result).not.toBeInstanceOf(NextResponse); - expect(result).toEqual({ mode: "list", accountId: "member_acc", limit: 20 }); + expect(result).toEqual({ mode: "list", accountId: MEMBER_ACCOUNT_ID, limit: 20 }); expect(validateAccountIdOverride).toHaveBeenCalledWith({ currentAccountId: "org_owner_acc", - targetAccountId: "member_acc", + targetAccountId: MEMBER_ACCOUNT_ID, }); }); @@ -183,7 +187,7 @@ describe("validateGetTaskRunQuery", () => { ); const request = createMockRequest( - "http://localhost:3000/api/tasks/runs?account_id=other_acc", + `http://localhost:3000/api/tasks/runs?account_id=${OTHER_ACCOUNT_ID}`, ); const result = await validateGetTaskRunQuery(request); @@ -196,19 +200,58 @@ describe("validateGetTaskRunQuery", () => { it("allows self-access via validateAccountIdOverride", async () => { vi.mocked(validateAuthContext).mockResolvedValue({ - accountId: "acc_123", + accountId: ACCOUNT_ID, orgId: null, authToken: "api-key", }); vi.mocked(checkIsAdmin).mockResolvedValue(false); - vi.mocked(validateAccountIdOverride).mockResolvedValue({ accountId: "acc_123" }); + vi.mocked(validateAccountIdOverride).mockResolvedValue({ accountId: ACCOUNT_ID }); - const request = createMockRequest("http://localhost:3000/api/tasks/runs?account_id=acc_123"); + const request = createMockRequest( + `http://localhost:3000/api/tasks/runs?account_id=${ACCOUNT_ID}`, + ); const result = await validateGetTaskRunQuery(request); expect(result).not.toBeInstanceOf(NextResponse); - expect(result).toEqual({ mode: "list", accountId: "acc_123", limit: 20 }); + expect(result).toEqual({ mode: "list", accountId: ACCOUNT_ID, limit: 20 }); + }); + + it("returns 400 for invalid UUID account_id", async () => { + vi.mocked(validateAuthContext).mockResolvedValue({ + accountId: ACCOUNT_ID, + orgId: null, + authToken: "api-key", + }); + + const request = createMockRequest( + "http://localhost:3000/api/tasks/runs?account_id=not-a-uuid", + ); + + const result = await validateGetTaskRunQuery(request); + + expect(result).toBeInstanceOf(NextResponse); + if (result instanceof NextResponse) { + expect(result.status).toBe(400); + } + }); + + it("trims whitespace from account_id before UUID validation", async () => { + vi.mocked(validateAuthContext).mockResolvedValue({ + accountId: "admin_acc", + orgId: null, + authToken: "bearer-token", + }); + vi.mocked(checkIsAdmin).mockResolvedValue(true); + + const request = createMockRequest( + `http://localhost:3000/api/tasks/runs?account_id=%20${OTHER_ACCOUNT_ID}%20`, + ); + + const result = await validateGetTaskRunQuery(request); + + expect(result).not.toBeInstanceOf(NextResponse); + expect(result).toEqual({ mode: "list", accountId: OTHER_ACCOUNT_ID, limit: 20 }); }); }); }); diff --git a/lib/tasks/validateGetTaskRunQuery.ts b/lib/tasks/validateGetTaskRunQuery.ts index b4bdf1694..7c1613adc 100644 --- a/lib/tasks/validateGetTaskRunQuery.ts +++ b/lib/tasks/validateGetTaskRunQuery.ts @@ -12,11 +12,7 @@ const getTaskRunQuerySchema = z.object({ .min(1) .transform(val => val.trim()) .optional(), - account_id: z - .string() - .min(1) - .transform(val => val.trim()) - .optional(), + account_id: z.string().trim().uuid("account_id must be a valid UUID").optional(), limit: z.coerce.number().int().min(1).max(100).default(20), });