Skip to content

Commit 3336a47

Browse files
committed
test(webapp): give the container-backed user-actor scope tests their own timeout
1 parent f700174 commit 3336a47

4 files changed

Lines changed: 142 additions & 110 deletions

File tree

apps/webapp/app/routes/api.v1.projects.$projectRef.runs.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,7 @@ export const loader = createLoaderPATApiRoute(
5252
project,
5353
searchParams,
5454
apiVersion,
55-
scope.scoped
56-
? { id: scope.environmentId, organizationId: scope.organizationId }
57-
: undefined
55+
scope.scoped ? { id: scope.environmentId, organizationId: scope.organizationId } : undefined
5856
);
5957

6058
if (!result) {

apps/webapp/app/services/personalAccessToken.server.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,7 @@ import { logger } from "./logger.server";
66
import { rbac } from "./rbac.server";
77
import { decryptToken, encryptToken, hashToken } from "~/utils/tokens.server";
88
import { env } from "~/env.server";
9-
import {
10-
isUserActorToken,
11-
type UserActorClaims,
12-
verifyUserActorToken,
13-
} from "@trigger.dev/rbac";
9+
import { isUserActorToken, type UserActorClaims, verifyUserActorToken } from "@trigger.dev/rbac";
1410

1511
const tokenValueLength = 40;
1612
//lowercase only, removed 0 and l to avoid confusion

apps/webapp/test/userActorProjectWideScope.test.ts

Lines changed: 78 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,20 @@ vi.mock("~/services/clickhouse/clickhouseFactoryInstance.server", () => ({
5757

5858
// The run list itself isn't under test — which environment the presenter is handed is.
5959
vi.mock("~/presenters/v3/ApiRunListPresenter.server", async () => {
60-
const actual = await vi.importActual<
61-
typeof import("~/presenters/v3/ApiRunListPresenter.server")
62-
>("~/presenters/v3/ApiRunListPresenter.server");
60+
const actual: any = await vi.importActual("~/presenters/v3/ApiRunListPresenter.server");
6361
return {
6462
ApiRunListSearchParams: actual.ApiRunListSearchParams,
6563
ApiRunListPresenter: class {
66-
async call(_project: unknown, _searchParams: unknown, _apiVersion: unknown, environment?: any) {
64+
async call(
65+
_project: unknown,
66+
_searchParams: unknown,
67+
_apiVersion: unknown,
68+
environment?: any
69+
) {
6770
ctx.presenterEnvironments.push(
68-
environment ? { id: environment.id, organizationId: environment.organizationId } : undefined
71+
environment
72+
? { id: environment.id, organizationId: environment.organizationId }
73+
: undefined
6974
);
7075
return { data: [] };
7176
}
@@ -77,7 +82,10 @@ vi.mock("~/presenters/v3/ApiRunListPresenter.server", async () => {
7782
vi.mock("~/services/rbac.server", async () => {
7883
const { buildJwtAbility, verifyUserActorToken } = await import("@trigger.dev/rbac");
7984
const bearerOf = (request: Request) =>
80-
request.headers.get("Authorization")?.replace(/^Bearer /, "").trim() ?? "";
85+
request.headers
86+
.get("Authorization")
87+
?.replace(/^Bearer /, "")
88+
.trim() ?? "";
8189

8290
return {
8391
rbac: {
@@ -205,25 +213,30 @@ postgresTest(
205213

206214
expect(scoped.status).toBe(200);
207215
expect(scoped.body.map((env: any) => env.id)).toEqual([seeded.envA.id]);
208-
}
216+
},
217+
60_000
209218
);
210219

211-
postgresTest("a user-actor token sees only its own environment's runs", async ({ prisma }) => {
212-
ctx.prisma = prisma;
213-
ctx.presenterEnvironments = [];
214-
const seeded = await seedProject(prisma);
215-
ctx.patUserId = seeded.user.id;
220+
postgresTest(
221+
"a user-actor token sees only its own environment's runs",
222+
async ({ prisma }) => {
223+
ctx.prisma = prisma;
224+
ctx.presenterEnvironments = [];
225+
const seeded = await seedProject(prisma);
226+
ctx.patUserId = seeded.user.id;
216227

217-
const scoped = await call(runsLoader, {
218-
projectRef: seeded.project.externalRef,
219-
token: await agentToken(seeded.user.id, seeded.envA.id),
220-
});
228+
const scoped = await call(runsLoader, {
229+
projectRef: seeded.project.externalRef,
230+
token: await agentToken(seeded.user.id, seeded.envA.id),
231+
});
221232

222-
expect(scoped.status).toBe(200);
223-
expect(ctx.presenterEnvironments).toEqual([
224-
{ id: seeded.envA.id, organizationId: seeded.organization.id },
225-
]);
226-
});
233+
expect(scoped.status).toBe(200);
234+
expect(ctx.presenterEnvironments).toEqual([
235+
{ id: seeded.envA.id, organizationId: seeded.organization.id },
236+
]);
237+
},
238+
60_000
239+
);
227240

228241
postgresTest(
229242
"a user-actor token asking for another environment is refused, not overridden",
@@ -242,47 +255,56 @@ postgresTest(
242255
expect(conflicting.status).toBe(403);
243256
expect(conflicting.body.code).toBe("forbidden_environment");
244257
expect(ctx.presenterEnvironments).toEqual([]);
245-
}
258+
},
259+
60_000
246260
);
247261

248-
postgresTest("a claimless user-actor token gets nothing project-wide", async ({ prisma }) => {
249-
ctx.prisma = prisma;
250-
const seeded = await seedProject(prisma);
251-
ctx.patUserId = seeded.user.id;
262+
postgresTest(
263+
"a claimless user-actor token gets nothing project-wide",
264+
async ({ prisma }) => {
265+
ctx.prisma = prisma;
266+
const seeded = await seedProject(prisma);
267+
ctx.patUserId = seeded.user.id;
252268

253-
// Any delegated client, not just the agent: a project-wide route has no scope to narrow to.
254-
const claimless = await call(environmentsLoader, {
255-
projectRef: seeded.project.externalRef,
256-
token: await agentToken(seeded.user.id, undefined, "mcp"),
257-
});
269+
// Any delegated client, not just the agent: a project-wide route has no scope to narrow to.
270+
const claimless = await call(environmentsLoader, {
271+
projectRef: seeded.project.externalRef,
272+
token: await agentToken(seeded.user.id, undefined, "mcp"),
273+
});
258274

259-
expect(claimless.status).toBe(403);
260-
expect(claimless.body.code).toBe("forbidden_environment");
261-
});
275+
expect(claimless.status).toBe(403);
276+
expect(claimless.body.code).toBe("forbidden_environment");
277+
},
278+
60_000
279+
);
262280

263-
postgresTest("a personal access token still gets the project-wide answer", async ({ prisma }) => {
264-
ctx.prisma = prisma;
265-
ctx.presenterEnvironments = [];
266-
const seeded = await seedProject(prisma);
267-
ctx.patUserId = seeded.user.id;
281+
postgresTest(
282+
"a personal access token still gets the project-wide answer",
283+
async ({ prisma }) => {
284+
ctx.prisma = prisma;
285+
ctx.presenterEnvironments = [];
286+
const seeded = await seedProject(prisma);
287+
ctx.patUserId = seeded.user.id;
268288

269-
const environments = await call(environmentsLoader, {
270-
projectRef: seeded.project.externalRef,
271-
token: PAT,
272-
});
289+
const environments = await call(environmentsLoader, {
290+
projectRef: seeded.project.externalRef,
291+
token: PAT,
292+
});
273293

274-
expect(environments.status).toBe(200);
275-
expect(environments.body.map((env: any) => env.id).sort()).toEqual(
276-
[seeded.envA.id, seeded.envB.id].sort()
277-
);
294+
expect(environments.status).toBe(200);
295+
expect(environments.body.map((env: any) => env.id).sort()).toEqual(
296+
[seeded.envA.id, seeded.envB.id].sort()
297+
);
278298

279-
const runs = await call(runsLoader, {
280-
projectRef: seeded.project.externalRef,
281-
token: PAT,
282-
search: `?filter[env]=${seeded.envB.slug}`,
283-
});
299+
const runs = await call(runsLoader, {
300+
projectRef: seeded.project.externalRef,
301+
token: PAT,
302+
search: `?filter[env]=${seeded.envB.slug}`,
303+
});
284304

285-
// No forced environment: the request's own filter decides, as before.
286-
expect(runs.status).toBe(200);
287-
expect(ctx.presenterEnvironments).toEqual([undefined]);
288-
});
305+
// No forced environment: the request's own filter decides, as before.
306+
expect(runs.status).toBe(200);
307+
expect(ctx.presenterEnvironments).toEqual([undefined]);
308+
},
309+
60_000
310+
);

apps/webapp/test/userActorTokenClaimsAndScopes.test.ts

Lines changed: 62 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ vi.mock("~/services/logger.server", () => ({
3636
vi.mock("~/services/rbac.server", async () => {
3737
const { buildJwtAbility, verifyUserActorToken } = await import("@trigger.dev/rbac");
3838
const bearerOf = (request: Request) =>
39-
request.headers.get("Authorization")?.replace(/^Bearer /, "").trim() ?? "";
39+
request.headers
40+
.get("Authorization")
41+
?.replace(/^Bearer /, "")
42+
.trim() ?? "";
4043

4144
return {
4245
rbac: {
@@ -195,56 +198,69 @@ postgresTest(
195198

196199
expect(denied.status).toBe(403);
197200
expect(denied.body.token).toBeUndefined();
198-
}
201+
},
202+
60_000
199203
);
200204

201-
postgresTest("a capless delegated token exchanges for a read-only JWT", async ({ prisma }) => {
202-
ctx.prisma = prisma;
203-
const seeded = await seedProject(prisma);
205+
postgresTest(
206+
"a capless delegated token exchanges for a read-only JWT",
207+
async ({ prisma }) => {
208+
ctx.prisma = prisma;
209+
const seeded = await seedProject(prisma);
204210

205-
const minted = await exchange({
206-
projectRef: seeded.project.externalRef,
207-
env: "prod",
208-
token: await token({ userId: seeded.user.id, environmentId: seeded.prod.id }),
209-
});
211+
const minted = await exchange({
212+
projectRef: seeded.project.externalRef,
213+
env: "prod",
214+
token: await token({ userId: seeded.user.id, environmentId: seeded.prod.id }),
215+
});
210216

211-
expect(minted.status).toBe(200);
212-
expect(payloadOf(minted.body.token).scopes).toEqual(["read:all"]);
213-
});
217+
expect(minted.status).toBe(200);
218+
expect(payloadOf(minted.body.token).scopes).toEqual(["read:all"]);
219+
},
220+
60_000
221+
);
214222

215-
postgresTest("the exchange clamps requested scopes to the token's cap", async ({ prisma }) => {
216-
ctx.prisma = prisma;
217-
const seeded = await seedProject(prisma);
218-
219-
const minted = await exchange({
220-
projectRef: seeded.project.externalRef,
221-
env: "prod",
222-
token: await token({
223-
userId: seeded.user.id,
224-
environmentId: seeded.prod.id,
225-
cap: ["read:runs", "read:apiKeys"],
226-
}),
227-
scopes: ["read:runs", "write:runs"],
228-
});
223+
postgresTest(
224+
"the exchange clamps requested scopes to the token's cap",
225+
async ({ prisma }) => {
226+
ctx.prisma = prisma;
227+
const seeded = await seedProject(prisma);
229228

230-
expect(minted.status).toBe(200);
231-
expect(payloadOf(minted.body.token).scopes).toEqual(["read:runs"]);
232-
});
229+
const minted = await exchange({
230+
projectRef: seeded.project.externalRef,
231+
env: "prod",
232+
token: await token({
233+
userId: seeded.user.id,
234+
environmentId: seeded.prod.id,
235+
cap: ["read:runs", "read:apiKeys"],
236+
}),
237+
scopes: ["read:runs", "write:runs"],
238+
});
233239

234-
postgresTest("the exchange only mints for the claimed environment", async ({ prisma }) => {
235-
ctx.prisma = prisma;
236-
const seeded = await seedProject(prisma);
237-
238-
const other = await exchange({
239-
projectRef: seeded.project.externalRef,
240-
env: "staging",
241-
token: await token({
242-
userId: seeded.user.id,
243-
environmentId: seeded.prod.id,
244-
cap: ["read:runs", "read:apiKeys"],
245-
}),
246-
});
240+
expect(minted.status).toBe(200);
241+
expect(payloadOf(minted.body.token).scopes).toEqual(["read:runs"]);
242+
},
243+
60_000
244+
);
247245

248-
expect(other.status).toBe(403);
249-
expect(other.body.token).toBeUndefined();
250-
});
246+
postgresTest(
247+
"the exchange only mints for the claimed environment",
248+
async ({ prisma }) => {
249+
ctx.prisma = prisma;
250+
const seeded = await seedProject(prisma);
251+
252+
const other = await exchange({
253+
projectRef: seeded.project.externalRef,
254+
env: "staging",
255+
token: await token({
256+
userId: seeded.user.id,
257+
environmentId: seeded.prod.id,
258+
cap: ["read:runs", "read:apiKeys"],
259+
}),
260+
});
261+
262+
expect(other.status).toBe(403);
263+
expect(other.body.token).toBeUndefined();
264+
},
265+
60_000
266+
);

0 commit comments

Comments
 (0)