Skip to content

Commit a611a65

Browse files
committed
add tests
1 parent 1c7ade7 commit a611a65

3 files changed

Lines changed: 597 additions & 0 deletions

File tree

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import { randomBytes } from "node:crypto";
2+
import {
3+
BulkActionStatus,
4+
BulkActionType,
5+
type PrismaClient,
6+
type Project,
7+
type RuntimeEnvironment,
8+
} from "@trigger.dev/database";
9+
import { describe, expect, it } from "vitest";
10+
import { getTestServer } from "./helpers/sharedTestServer";
11+
import { seedTestEnvironment } from "./helpers/seedTestEnvironment";
12+
13+
describe("Bulk actions API", () => {
14+
it("lists bulk actions with cursor pagination", async () => {
15+
const server = getTestServer();
16+
const { apiKey, project, environment } = await seedTestEnvironment(server.prisma);
17+
18+
const oldest = await seedBulkAction(server.prisma, project, environment, {
19+
name: "Oldest",
20+
createdAt: new Date("2026-07-01T10:00:00.000Z"),
21+
});
22+
const middle = await seedBulkAction(server.prisma, project, environment, {
23+
name: "Middle",
24+
createdAt: new Date("2026-07-01T10:01:00.000Z"),
25+
});
26+
const latest = await seedBulkAction(server.prisma, project, environment, {
27+
name: "Latest",
28+
createdAt: new Date("2026-07-01T10:02:00.000Z"),
29+
});
30+
31+
const firstResponse = await server.webapp.fetch("/api/v1/bulk-actions?page[size]=2", {
32+
headers: authHeaders(apiKey),
33+
});
34+
expect(firstResponse.status).toBe(200);
35+
const firstPage = await firstResponse.json();
36+
expect(firstPage.data.map((item: { id: string }) => item.id)).toEqual([
37+
latest.friendlyId,
38+
middle.friendlyId,
39+
]);
40+
expect(firstPage.pagination.next).toEqual(expect.any(String));
41+
expect(firstPage.pagination.previous).toBeUndefined();
42+
43+
const secondResponse = await server.webapp.fetch(
44+
`/api/v1/bulk-actions?page[size]=2&page[after]=${encodeURIComponent(
45+
firstPage.pagination.next
46+
)}`,
47+
{ headers: authHeaders(apiKey) }
48+
);
49+
expect(secondResponse.status).toBe(200);
50+
const secondPage = await secondResponse.json();
51+
expect(secondPage.data.map((item: { id: string }) => item.id)).toEqual([oldest.friendlyId]);
52+
expect(secondPage.pagination.next).toBeUndefined();
53+
expect(secondPage.pagination.previous).toEqual(expect.any(String));
54+
55+
const previousResponse = await server.webapp.fetch(
56+
`/api/v1/bulk-actions?page[size]=2&page[before]=${encodeURIComponent(
57+
secondPage.pagination.previous
58+
)}`,
59+
{ headers: authHeaders(apiKey) }
60+
);
61+
expect(previousResponse.status).toBe(200);
62+
const previousPage = await previousResponse.json();
63+
expect(previousPage.data.map((item: { id: string }) => item.id)).toEqual([
64+
latest.friendlyId,
65+
middle.friendlyId,
66+
]);
67+
});
68+
69+
it("retrieves a bulk action in the authenticated environment", async () => {
70+
const server = getTestServer();
71+
const { apiKey, project, environment } = await seedTestEnvironment(server.prisma);
72+
const bulkAction = await seedBulkAction(server.prisma, project, environment, {
73+
name: "Retrieve me",
74+
type: BulkActionType.REPLAY,
75+
status: BulkActionStatus.COMPLETED,
76+
totalCount: 4,
77+
successCount: 3,
78+
failureCount: 1,
79+
completedAt: new Date("2026-07-01T10:05:00.000Z"),
80+
});
81+
82+
const response = await server.webapp.fetch(`/api/v1/bulk-actions/${bulkAction.friendlyId}`, {
83+
headers: authHeaders(apiKey),
84+
});
85+
86+
expect(response.status).toBe(200);
87+
const body = await response.json();
88+
expect(body).toMatchObject({
89+
id: bulkAction.friendlyId,
90+
name: "Retrieve me",
91+
type: "REPLAY",
92+
status: "COMPLETED",
93+
counts: { total: 4, success: 3, failure: 1 },
94+
});
95+
expect(body.createdAt).toEqual(expect.any(String));
96+
expect(body.completedAt).toEqual("2026-07-01T10:05:00.000Z");
97+
});
98+
99+
it("does not retrieve bulk actions from another environment", async () => {
100+
const server = getTestServer();
101+
const a = await seedTestEnvironment(server.prisma);
102+
const b = await seedTestEnvironment(server.prisma);
103+
const bulkAction = await seedBulkAction(server.prisma, a.project, a.environment, {
104+
name: "Other environment",
105+
});
106+
107+
const response = await server.webapp.fetch(`/api/v1/bulk-actions/${bulkAction.friendlyId}`, {
108+
headers: authHeaders(b.apiKey),
109+
});
110+
111+
expect(response.status).toBe(404);
112+
});
113+
114+
it("aborts a pending bulk action", async () => {
115+
const server = getTestServer();
116+
const { apiKey, project, environment } = await seedTestEnvironment(server.prisma);
117+
const bulkAction = await seedBulkAction(server.prisma, project, environment, {
118+
status: BulkActionStatus.PENDING,
119+
});
120+
121+
const response = await server.webapp.fetch(
122+
`/api/v1/bulk-actions/${bulkAction.friendlyId}/abort`,
123+
{ method: "POST", headers: authHeaders(apiKey) }
124+
);
125+
126+
expect(response.status).toBe(200);
127+
await expect(response.json()).resolves.toEqual({ id: bulkAction.friendlyId });
128+
129+
const updated = await server.prisma.bulkActionGroup.findUniqueOrThrow({
130+
where: { id: bulkAction.id },
131+
select: { status: true },
132+
});
133+
expect(updated.status).toBe(BulkActionStatus.ABORTED);
134+
});
135+
136+
it("returns a safe validation error when aborting a completed bulk action", async () => {
137+
const server = getTestServer();
138+
const { apiKey, project, environment } = await seedTestEnvironment(server.prisma);
139+
const bulkAction = await seedBulkAction(server.prisma, project, environment, {
140+
status: BulkActionStatus.COMPLETED,
141+
completedAt: new Date("2026-07-01T10:05:00.000Z"),
142+
});
143+
144+
const response = await server.webapp.fetch(
145+
`/api/v1/bulk-actions/${bulkAction.friendlyId}/abort`,
146+
{ method: "POST", headers: authHeaders(apiKey) }
147+
);
148+
149+
expect(response.status).toBe(409);
150+
const body = await response.json();
151+
expect(body).toEqual({ error: "Bulk action is already completed" });
152+
expect(JSON.stringify(body)).not.toContain(bulkAction.friendlyId);
153+
});
154+
155+
it("rejects create requests with both filter and runIds", async () => {
156+
const server = getTestServer();
157+
const { apiKey } = await seedTestEnvironment(server.prisma);
158+
159+
const response = await server.webapp.fetch("/api/v1/bulk-actions", {
160+
method: "POST",
161+
headers: authHeaders(apiKey),
162+
body: JSON.stringify({ action: "cancel", filter: {}, runIds: ["run_123"] }),
163+
});
164+
165+
expect(response.status).toBe(400);
166+
const body = await response.json();
167+
expect(body.error).toContain("Exactly one of filter or runIds must be provided");
168+
});
169+
170+
it("returns a generic error for unexpected create failures", async () => {
171+
const server = getTestServer();
172+
const { apiKey } = await seedTestEnvironment(server.prisma);
173+
174+
const response = await server.webapp.fetch("/api/v1/bulk-actions", {
175+
method: "POST",
176+
headers: authHeaders(apiKey),
177+
body: JSON.stringify({ action: "cancel", filter: {}, name: "No ClickHouse in this suite" }),
178+
});
179+
180+
expect(response.status).toBe(500);
181+
await expect(response.json()).resolves.toEqual({ error: "Failed to create bulk action" });
182+
});
183+
});
184+
185+
function authHeaders(apiKey: string) {
186+
return {
187+
Authorization: `Bearer ${apiKey}`,
188+
"Content-Type": "application/json",
189+
};
190+
}
191+
192+
async function seedBulkAction(
193+
prisma: PrismaClient,
194+
project: Pick<Project, "id">,
195+
environment: Pick<RuntimeEnvironment, "id">,
196+
overrides: {
197+
name?: string;
198+
type?: BulkActionType;
199+
status?: BulkActionStatus;
200+
createdAt?: Date;
201+
completedAt?: Date;
202+
totalCount?: number;
203+
successCount?: number;
204+
failureCount?: number;
205+
} = {}
206+
) {
207+
return prisma.bulkActionGroup.create({
208+
data: {
209+
friendlyId: `bulk_${randomHex(16)}`,
210+
projectId: project.id,
211+
environmentId: environment.id,
212+
name: overrides.name ?? "Test bulk action",
213+
type: overrides.type ?? BulkActionType.CANCEL,
214+
status: overrides.status ?? BulkActionStatus.PENDING,
215+
queryName: "bulk_action_v1",
216+
params: {},
217+
totalCount: overrides.totalCount ?? 1,
218+
successCount: overrides.successCount ?? 0,
219+
failureCount: overrides.failureCount ?? 0,
220+
createdAt: overrides.createdAt,
221+
completedAt: overrides.completedAt,
222+
},
223+
});
224+
}
225+
226+
function randomHex(length: number) {
227+
return randomBytes(Math.ceil(length / 2))
228+
.toString("hex")
229+
.slice(0, length);
230+
}

0 commit comments

Comments
 (0)