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
15 changes: 8 additions & 7 deletions app/api/tasks/runs/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
65 changes: 54 additions & 11 deletions lib/tasks/__tests__/validateGetTaskRunQuery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
});
Expand All @@ -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,
});
});

Expand All @@ -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);
Expand All @@ -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 });
});
});
});
6 changes: 1 addition & 5 deletions lib/tasks/validateGetTaskRunQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
});

Expand Down
Loading