-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
test: e2e auth baseline tests + webapp testcontainer infrastructure #3438
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
matt-aitken
wants to merge
10
commits into
main
Choose a base branch
from
e2e-webapp-auth-baseline
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+447
−1
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
3fe654a
test: add e2e webapp auth baseline and testcontainer infrastructure
matt-aitken 7292806
Potential fix for pull request finding 'CodeQL / Insecure randomness'
matt-aitken b3c3f15
fix: address Devin Review bugs in webapp testcontainer
devin-ai-integration[bot] ef83853
test: exclude *.e2e.test.ts from standard vitest shards
matt-aitken 121cda1
ci: add dedicated e2e webapp job that builds first then runs auth tests
matt-aitken 8413af5
fix: use path.delimiter instead of hard-coded colon in NODE_PATH join
matt-aitken 419a2df
fix: import vi explicitly in api-auth e2e test
matt-aitken 9e9ff32
fix: best-effort teardown in stop() to prevent resource leaks
matt-aitken 407a3c0
fix: add vitest.e2e.config.ts so e2e tests aren't excluded in CI
matt-aitken 90f7f13
fix: add dummy REDIS_HOST/PORT so webapp passes module-level validati…
matt-aitken File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| name: "🧪 E2E Tests: Webapp" | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| on: | ||
| workflow_call: | ||
|
|
||
| jobs: | ||
| e2eTests: | ||
| name: "🧪 E2E Tests: Webapp" | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 20 | ||
| env: | ||
| DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} | ||
| steps: | ||
| - name: 🔧 Disable IPv6 | ||
| run: | | ||
| sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1 | ||
| sudo sysctl -w net.ipv6.conf.default.disable_ipv6=1 | ||
| sudo sysctl -w net.ipv6.conf.lo.disable_ipv6=1 | ||
|
|
||
| - name: 🔧 Configure docker address pool | ||
| run: | | ||
| CONFIG='{ | ||
| "default-address-pools" : [ | ||
| { | ||
| "base" : "172.17.0.0/12", | ||
| "size" : 20 | ||
| }, | ||
| { | ||
| "base" : "192.168.0.0/16", | ||
| "size" : 24 | ||
| } | ||
| ] | ||
| }' | ||
| mkdir -p /etc/docker | ||
| echo "$CONFIG" | sudo tee /etc/docker/daemon.json | ||
|
|
||
| - name: 🔧 Restart docker daemon | ||
| run: sudo systemctl restart docker | ||
|
|
||
| - name: ⬇️ Checkout repo | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: ⎔ Setup pnpm | ||
| uses: pnpm/action-setup@v4 | ||
| with: | ||
| version: 10.23.0 | ||
|
|
||
| - name: ⎔ Setup node | ||
| uses: buildjet/setup-node@v4 | ||
| with: | ||
| node-version: 20.20.0 | ||
| cache: "pnpm" | ||
|
|
||
| # ..to avoid rate limits when pulling images | ||
| - name: 🐳 Login to DockerHub | ||
| if: ${{ env.DOCKERHUB_USERNAME }} | ||
| uses: docker/login-action@v3 | ||
| with: | ||
| username: ${{ secrets.DOCKERHUB_USERNAME }} | ||
| password: ${{ secrets.DOCKERHUB_TOKEN }} | ||
| - name: 🐳 Skipping DockerHub login (no secrets available) | ||
| if: ${{ !env.DOCKERHUB_USERNAME }} | ||
| run: echo "DockerHub login skipped because secrets are not available." | ||
|
|
||
| - name: 🐳 Pre-pull testcontainer images | ||
| if: ${{ env.DOCKERHUB_USERNAME }} | ||
| run: | | ||
| echo "Pre-pulling Docker images with authenticated session..." | ||
| docker pull postgres:14 | ||
| docker pull testcontainers/ryuk:0.11.0 | ||
| echo "Image pre-pull complete" | ||
|
|
||
| - name: 📥 Download deps | ||
| run: pnpm install --frozen-lockfile | ||
|
|
||
| - name: 📀 Generate Prisma Client | ||
| run: pnpm run generate | ||
|
|
||
| - name: 🏗️ Build Webapp | ||
| run: pnpm run build --filter webapp | ||
|
|
||
| - name: 🧪 Run Webapp E2E Tests | ||
| run: cd apps/webapp && pnpm exec vitest run --config vitest.e2e.config.ts --reporter=default |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| /** | ||
| * E2E auth baseline tests. | ||
| * | ||
| * These tests capture current auth behavior before the apiBuilder migration to RBAC. | ||
| * Run them before and after the migration to verify behavior is identical. | ||
| * | ||
| * Requires a pre-built webapp: pnpm run build --filter webapp | ||
| */ | ||
| import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; | ||
| import type { TestServer } from "@internal/testcontainers/webapp"; | ||
| import { startTestServer } from "@internal/testcontainers/webapp"; | ||
| import { generateJWT } from "@trigger.dev/core/v3/jwt"; | ||
| import { seedTestEnvironment } from "./helpers/seedTestEnvironment"; | ||
|
|
||
| vi.setConfig({ testTimeout: 180_000 }); | ||
|
|
||
| // Shared across all tests in this file — one postgres container + one webapp instance. | ||
| let server: TestServer; | ||
|
|
||
| beforeAll(async () => { | ||
| server = await startTestServer(); | ||
| }, 180_000); | ||
|
|
||
| afterAll(async () => { | ||
| await server?.stop(); | ||
| }, 120_000); | ||
|
|
||
| async function generateTestJWT( | ||
| environment: { id: string; apiKey: string }, | ||
| options: { scopes?: string[] } = {} | ||
| ): Promise<string> { | ||
| const scopes = options.scopes ?? ["read:runs"]; | ||
| return generateJWT({ | ||
| secretKey: environment.apiKey, | ||
| payload: { pub: true, sub: environment.id, scopes }, | ||
| expirationTime: "15m", | ||
| }); | ||
| } | ||
|
|
||
| describe("API bearer auth — baseline behavior", () => { | ||
| it("valid API key: auth passes (404 not 401)", async () => { | ||
| const { apiKey } = await seedTestEnvironment(server.prisma); | ||
| const res = await server.webapp.fetch("/api/v1/runs/run_doesnotexist/result", { | ||
| headers: { Authorization: `Bearer ${apiKey}` }, | ||
| }); | ||
| // Auth passed — resource just doesn't exist | ||
| expect(res.status).not.toBe(401); | ||
| expect(res.status).not.toBe(403); | ||
| }); | ||
|
|
||
| it("missing Authorization header: 401", async () => { | ||
| const res = await server.webapp.fetch("/api/v1/runs/run_doesnotexist/result"); | ||
| expect(res.status).toBe(401); | ||
| }); | ||
|
|
||
| it("invalid API key: 401", async () => { | ||
| const res = await server.webapp.fetch("/api/v1/runs/run_doesnotexist/result", { | ||
| headers: { Authorization: "Bearer tr_dev_completely_invalid_key_xyz_not_real" }, | ||
| }); | ||
| expect(res.status).toBe(401); | ||
| }); | ||
|
|
||
| it("401 response has error field", async () => { | ||
| const res = await server.webapp.fetch("/api/v1/runs/run_doesnotexist/result"); | ||
| const body = await res.json(); | ||
| expect(body).toHaveProperty("error"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("JWT bearer auth — baseline behavior", () => { | ||
| it("valid JWT on JWT-enabled route: auth passes", async () => { | ||
| const { environment } = await seedTestEnvironment(server.prisma); | ||
| const jwt = await generateTestJWT(environment, { scopes: ["read:runs"] }); | ||
|
|
||
| // /api/v1/runs has allowJWT: true with superScopes: ["read:runs", ...] | ||
| const res = await server.webapp.fetch("/api/v1/runs", { | ||
| headers: { Authorization: `Bearer ${jwt}` }, | ||
| }); | ||
|
|
||
| // Auth passed — 200 (empty list) or 400 (bad search params), not 401 | ||
| expect(res.status).not.toBe(401); | ||
| }); | ||
|
|
||
| it("valid JWT on non-JWT route: 401", async () => { | ||
| const { environment } = await seedTestEnvironment(server.prisma); | ||
| const jwt = await generateTestJWT(environment, { scopes: ["read:runs"] }); | ||
|
|
||
| // /api/v1/runs/$runParam/result does NOT have allowJWT: true | ||
| const res = await server.webapp.fetch("/api/v1/runs/run_doesnotexist/result", { | ||
| headers: { Authorization: `Bearer ${jwt}` }, | ||
| }); | ||
|
|
||
| expect(res.status).toBe(401); | ||
| }); | ||
|
|
||
| it("JWT with empty scopes on JWT-enabled route: 403", async () => { | ||
| const { environment } = await seedTestEnvironment(server.prisma); | ||
| const jwt = await generateTestJWT(environment, { scopes: [] }); | ||
|
|
||
| const res = await server.webapp.fetch("/api/v1/runs", { | ||
| headers: { Authorization: `Bearer ${jwt}` }, | ||
| }); | ||
|
|
||
| // Empty scopes → no read:runs permission → 403 | ||
| expect(res.status).toBe(403); | ||
| }); | ||
|
|
||
| it("JWT signed with wrong key: 401", async () => { | ||
| const { environment } = await seedTestEnvironment(server.prisma); | ||
| const jwt = await generateJWT({ | ||
| secretKey: "wrong-signing-key-that-does-not-match-environment-key", | ||
| payload: { pub: true, sub: environment.id, scopes: ["read:runs"] }, | ||
| }); | ||
|
|
||
| const res = await server.webapp.fetch("/api/v1/runs", { | ||
| headers: { Authorization: `Bearer ${jwt}` }, | ||
| }); | ||
|
|
||
| expect(res.status).toBe(401); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import type { PrismaClient } from "@trigger.dev/database"; | ||
| import { randomBytes } from "crypto"; | ||
|
|
||
| function randomHex(len = 12): string { | ||
| return randomBytes(Math.ceil(len / 2)).toString("hex").slice(0, len); | ||
| } | ||
|
|
||
| export async function seedTestEnvironment(prisma: PrismaClient) { | ||
| const suffix = randomHex(8); | ||
| const apiKey = `tr_dev_${randomHex(24)}`; | ||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| const pkApiKey = `pk_dev_${randomHex(24)}`; | ||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| const organization = await prisma.organization.create({ | ||
| data: { | ||
| title: `e2e-test-org-${suffix}`, | ||
| slug: `e2e-org-${suffix}`, | ||
| v3Enabled: true, | ||
| }, | ||
| }); | ||
|
|
||
| const project = await prisma.project.create({ | ||
| data: { | ||
| name: `e2e-test-project-${suffix}`, | ||
| slug: `e2e-proj-${suffix}`, | ||
| externalRef: `proj_${suffix}`, | ||
| organizationId: organization.id, | ||
| engine: "V2", | ||
| }, | ||
| }); | ||
|
|
||
| const environment = await prisma.runtimeEnvironment.create({ | ||
| data: { | ||
| slug: "dev", | ||
| type: "DEVELOPMENT", | ||
| apiKey, | ||
| pkApiKey, | ||
| shortcode: suffix.slice(0, 4), | ||
| projectId: project.id, | ||
| organizationId: organization.id, | ||
| }, | ||
| }); | ||
|
|
||
| return { organization, project, environment, apiKey }; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { defineConfig } from "vitest/config"; | ||
| import tsconfigPaths from "vite-tsconfig-paths"; | ||
|
|
||
| export default defineConfig({ | ||
| test: { | ||
| include: ["test/**/*.e2e.test.ts"], | ||
| globals: true, | ||
| pool: "forks", | ||
| }, | ||
| // @ts-ignore | ||
| plugins: [tsconfigPaths({ projects: ["./tsconfig.json"] })], | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚩 E2E tests run in the unit-tests workflow rather than a dedicated pipeline
The new
e2e-webapp.ymlworkflow is called from.github/workflows/unit-tests.ymlalongside the actual unit test jobs. E2E tests (starting containers, building the webapp, running a server) are significantly heavier and slower than unit tests. This coupling means E2E failures block the unit tests workflow. This is a design choice that may be intentional for simplicity, but a dedicated workflow or a separate CI job group might be more appropriate as E2E tests grow.Was this helpful? React with 👍 or 👎 to provide feedback.