Skip to content

Commit ee6bce6

Browse files
matt-aitkenclaude
andcommitted
test(webapp): e2e verify /healthcheck returns 500 with REQUIRE_PLUGINS=1
Closes the loop on the unit-test loader coverage by spawning a real webapp + DB + Redis and verifying via HTTP: - REQUIRE_PLUGINS=1 + plugin missing → GET /healthcheck → 500 (so ECS/k8s readiness probes fail and the rollout rolls back). - REQUIRE_PLUGINS unset + plugin missing → GET /healthcheck → 200 (baseline — self-hoster behaviour unchanged). - internal-packages/testcontainers/src/webapp.ts: adds `requirePlugins?: boolean` to StartWebappOptions. Implies `forceRbacFallback: false` (you can't observe the throw if the loader short-circuits to the fallback). - apps/webapp/test/healthcheck-require-plugins.e2e.test.ts: two describes, each with its own webapp instance (~30s spawn each) since they need different env. Auto-picked up by vitest.e2e.config.ts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1ce122a commit ee6bce6

2 files changed

Lines changed: 81 additions & 1 deletion

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* E2E verification that REQUIRE_PLUGINS=1 fails the rollout via /healthcheck.
3+
*
4+
* The unit tests in @trigger.dev/rbac cover the loader throw. This file
5+
* closes the loop end-to-end: spawn a real webapp, hit /healthcheck via
6+
* HTTP, and verify the route's catch turns the throw into a 500 — the
7+
* status the ECS/k8s readiness probe rolls back on.
8+
*
9+
* Each case spawns its own webapp + Postgres + Redis container (~30s) so
10+
* env can differ per case. Slow but isolated, matching api-auth.e2e.test.ts.
11+
*
12+
* Requires a pre-built webapp: pnpm run build --filter webapp
13+
*/
14+
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
15+
import type { TestServer } from "@internal/testcontainers/webapp";
16+
import { startTestServer } from "@internal/testcontainers/webapp";
17+
18+
vi.setConfig({ testTimeout: 180_000 });
19+
20+
describe("/healthcheck with REQUIRE_PLUGINS", () => {
21+
describe("REQUIRE_PLUGINS=1 + plugin missing", () => {
22+
let server: TestServer;
23+
24+
beforeAll(async () => {
25+
// requirePlugins: true implies forceRbacFallback: false, so the
26+
// loader actually tries to dynamic-import the plugin. The plugin
27+
// is not installed in this OSS repo, so the import fails and the
28+
// loader throws (instead of falling back) because REQUIRE_PLUGINS=1.
29+
// The throw surfaces on the first .isUsingPlugin() call from the
30+
// /healthcheck route, which catches it and returns 500.
31+
server = await startTestServer({ requirePlugins: true });
32+
}, 180_000);
33+
34+
afterAll(async () => {
35+
await server?.stop();
36+
}, 120_000);
37+
38+
it("returns 500 so the readiness probe fails and the rollout is rolled back", async () => {
39+
const res = await server.webapp.fetch("/healthcheck");
40+
expect(res.status).toBe(500);
41+
expect(await res.text()).toBe("ERROR");
42+
});
43+
});
44+
45+
describe("REQUIRE_PLUGINS unset + plugin missing", () => {
46+
let server: TestServer;
47+
48+
beforeAll(async () => {
49+
// Default: forceRbacFallback=true so the loader short-circuits to
50+
// the fallback without trying to import. /healthcheck succeeds.
51+
server = await startTestServer();
52+
}, 180_000);
53+
54+
afterAll(async () => {
55+
await server?.stop();
56+
}, 120_000);
57+
58+
it("returns 200 (baseline — unchanged self-hoster behaviour)", async () => {
59+
const res = await server.webapp.fetch("/healthcheck");
60+
expect(res.status).toBe(200);
61+
expect(await res.text()).toBe("OK");
62+
});
63+
});
64+
});

internal-packages/testcontainers/src/webapp.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ export interface StartWebappOptions {
4949
* plugin instead, for testing the plugin path.
5050
*/
5151
forceRbacFallback?: boolean;
52+
53+
/**
54+
* When true, spawns the webapp with `REQUIRE_PLUGINS=1` so the plugin
55+
* loader throws instead of silently falling back when the plugin
56+
* module fails to load. Used by the healthcheck rollback e2e test —
57+
* with this set and the plugin not installed, `/healthcheck` should
58+
* return 500.
59+
*
60+
* Implies `forceRbacFallback: false` (you can't observe REQUIRE_PLUGINS
61+
* behaviour when the loader is short-circuited).
62+
*/
63+
requirePlugins?: boolean;
5264
}
5365

5466
export async function startWebapp(
@@ -59,7 +71,10 @@ export async function startWebapp(
5971
instance: WebappInstance;
6072
stop: () => Promise<void>;
6173
}> {
62-
const forceRbacFallback = options.forceRbacFallback ?? true;
74+
// requirePlugins implies forceRbacFallback=false — you can't observe the
75+
// REQUIRE_PLUGINS=1 throw if the loader short-circuits before reaching it.
76+
const requirePlugins = options.requirePlugins ?? false;
77+
const forceRbacFallback = options.forceRbacFallback ?? !requirePlugins;
6378
const port = await findFreePort();
6479

6580
// Merge NODE_PATH so transitive pnpm deps (hoisted to .pnpm/node_modules) are resolvable
@@ -107,6 +122,7 @@ export async function startWebapp(
107122
// plugin is installed in the local node_modules. Set to "0" /
108123
// undefined to spawn a webapp that loads any installed plugin.
109124
...(forceRbacFallback ? { RBAC_FORCE_FALLBACK: "1" } : {}),
125+
...(requirePlugins ? { REQUIRE_PLUGINS: "1" } : {}),
110126
NODE_PATH: nodePath,
111127
},
112128
stdio: ["ignore", "pipe", "pipe"],

0 commit comments

Comments
 (0)