Skip to content

Instantly share code, notes, and snippets.

@aatchison
Last active February 23, 2026 17:28
Show Gist options
  • Select an option

  • Save aatchison/f4bbf2fa5a7b796241fa2969d00e9d26 to your computer and use it in GitHub Desktop.

Select an option

Save aatchison/f4bbf2fa5a7b796241fa2969d00e9d26 to your computer and use it in GitHub Desktop.
E2E testing setup for tbpro-add-on (DinD/devcontainer quirks)

E2E / Integration Testing — tbpro-add-on (DinD/devcontainer quirks)

PR: fix/e2e-compose-dind → main
Last updated: 2026-02-23


Overview

E2E tests run the full stack (postgres, backend, reverse-proxy, frontend) via Docker Compose and exercise the app with Playwright against Firefox. Integration tests run vitest inside the backend container against a live database.

  • E2E tests live in packages/send/e2e/, tagged @dev-desktop. Entry point: scripts/e2e.sh (invoked by lerna run test:e2e:ci --scope=send-suite-e2e).
  • Integration tests run via pnpm run test:integration:ciscripts/integration.sh (invoked by lerna run test:integration:ci).

Why compose.ci.yml exists

The default compose.yml mounts named Docker volumes over the node_modules directories inside each container:

volumes:
  - ./:/app
  - frontend-node-modules:/app/node_modules

This is useful for local development but breaks in Docker-in-Docker environments (devpod, GitHub Actions with DinD). Named volumes initialise empty instead of copying from the image layer. The empty volume shadows the node_modules installed during docker build, leaving packages like @sentry/vite-plugin missing at runtime:

Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@sentry/vite-plugin'

Solution

compose.ci.yml removes all named node_modules volumes and source bind mounts. Containers run directly from their built image layers.

Also, env_file: is used for the backend instead of a volumes: bind mount for .env, avoiding DinD path-resolution failures that leave bind-mounted files empty.


GitHub Actions devcontainer quirks

The container: job runs inside ghcr.io/.../cached-devcontainer:latest. Several non-obvious issues arise:

Firefox refuses to run as root with a non-root HOME

GitHub Actions sets HOME=/github/home (owned by uid 1001) even though the devcontainer container runs as root. Firefox refuses this combination.

scripts/e2e.sh works around this by:

  1. Pinning PLAYWRIGHT_BROWSERS_PATH to the original $HOME/.cache/ms-playwright at startup (before any HOME change)
  2. Setting HOME=/root immediately before launching Playwright

Firefox sandbox syscalls blocked by seccomp

The devcontainer's seccomp profile blocks clone(CLONE_NEWUSER) and similar syscalls Firefox needs for its sandbox. Fixed by adding --security-opt seccomp=unconfined to the container options: in the workflow YAML.

/dev/shm too small

Docker's default /dev/shm (64 MB) is too small for Firefox. Fixed by --shm-size=2gb in container options.


DinD networking: GitHub Actions vs devpod

Two different DinD setups require different host addresses for published ports:

Environment Docker setup Published ports accessible via
GitHub Actions container: Host socket bind-mounted Bridge gateway IP (ip route show default | awk '{print $3; exit}')
Devpod Internal docker daemon localhost

scripts/e2e.sh and scripts/integration.sh detect this via GITHUB_ACTIONS=true.


TLS cert hostname mismatch — root cause of register-button never appearing

The problem

The nginx reverse-proxy TLS cert (tls-dev-proxy/certs/localhost.crt) is issued for CN=localhost and SAN=DNS:localhost only. It is also expired (Dec 4, 2024).

In GitHub Actions CI the compose containers publish ports to the HOST's bridge IP (e.g. 172.18.0.1), not to localhost inside the CI container. The previous fix patched frontend/.env to point VITE_SEND_SERVER_URL at https://172.18.0.1:8088, but this caused Firefox to make TLS connections to 172.18.0.1:8088:

  • Hostname mismatch: cert is for localhost, not 172.18.0.1
  • Expired cert: Dec 2024
  • Firefox hangs on SSL even with ignoreHTTPSErrors: true (hostname mismatch bypasses that flag)

The hang prevents validateToken() from completing → router never redirects to /loginPublicLogin.vue never renders → register-button locator times out.

Solution: Node.js TCP proxy

After servers are ready, scripts/e2e.sh starts a Node.js TCP proxy (in GitHub Actions CI only):

localhost:5173  →  DOCKER_HOST:5173   (Vite dev server)
localhost:8088  →  DOCKER_HOST:8088   (nginx/HTTPS reverse proxy)

Firefox connects to localhost:{5173,8088} — the cert CN matches, ignoreHTTPSErrors handles the expired/self-signed cert, and SSL succeeds. CORS also passes because the browser origin becomes http://localhost:5173, which is in SEND_BACKEND_CORS_ORIGINS.

node -e "
const net = require('net');
const host = process.argv[1];
[[5173, 5173], [8088, 8088]].forEach(([lp, rp]) => {
  net.createServer(c => {
    const r = net.connect(rp, host);
    c.pipe(r); r.pipe(c);
    c.on('error', () => r.destroy());
    r.on('error', () => c.destroy());
  }).listen(lp, '127.0.0.1', () =>
    process.stdout.write('Proxy ready: localhost:' + lp + ' -> ' + host + ':' + rp + '\n'));
});" "${DOCKER_HOST}" &
TCP_PROXY_PID=$!

The sed patches to frontend/.env were removed — the frontend keeps its default VITE_SEND_SERVER_URL=https://localhost:8088, which works via the proxy.

PLAYWRIGHT_BASE_URL is unconditionally set to http://localhost:5173.


Integration test fixes

npm install vitest fails (ERESOLVE)

The old scripts/integration.sh ran docker compose exec backend npm install vitest to install vitest inside the container. This triggered an ERESOLVE peer-dependency conflict because the project is managed by pnpm.

Fix: removed the npm install step. Vitest is already in devDependencies and is installed by pnpm install during the Docker build step, so npx vitest works without re-installing.

vitest.integration.config.js not found in container

The Dockerfile ADD statements only copied src/, scripts/, public/, prisma/, package.json, and tsconfig.json. The integration config file at the repo root of packages/send/backend/ was not included, so vitest failed with:

✘ [ERROR] Could not resolve "/app/vitest.integration.config.js"

Fix: added to packages/send/backend/Dockerfile:

ADD vitest.integration.config.js ./

CI step timeout: 10 → 20 minutes

The Run stack and test step previously had timeout-minutes: 10. The step must fit:

  1. Playwright browser install (playwright install --with-deps) — ~1–2 min
  2. Docker image build (lerna run build-image:backend) — ~3–5 min
  3. Server startup + health checks — ~1–2 min
  4. Test execution — ~3–7 min

Total often exceeds 10 minutes. Increased to timeout-minutes: 20 in e2e-test.yml, e2e-bucket-test.yml, and integration-test.yml.


ESLint crash fix

package.json (backend) called pnpx eslint for lint:all. pnpx is not available in the CI container (only pnpm is). Changed to pnpm exec eslint.


Current CI status (as of 2026-02-23)

Workflow Result Notes
integration-tests ✅ green ~4 min
e2e-tests ⚠️ 7/8 pass "Login using OIDC" requires real TBPRO_USERNAME/TBPRO_PASSWORD secrets
e2e-bucket-tests ⚠️ same Same OIDC test fails
validate-devcontainer ✅ green
validate ❌ s3/backblaze unit tests Requires real S3/B2 credentials (pre-existing)

The one remaining e2e failure is credential-gated, not infrastructure. The OIDC login test (OIDC flow › Login using OIDC) attempts a real Thunderbird Pro OIDC flow that requires secrets not set in the public CI environment. All other tests (register, login, file upload/download/share/delete, key restore) pass.


Complete list of files changed in PR

File Change
scripts/e2e.sh TCP proxy approach; remove sed patches; unconditional PLAYWRIGHT_BASE_URL=http://localhost:5173; HOME=/root fix; PLAYWRIGHT_BROWSERS_PATH pin
scripts/integration.sh Remove npm install vitest; use DOCKER_HOST for health checks and compose exec
packages/send/backend/Dockerfile Add vitest.integration.config.js
packages/send/backend/package.json pnpx eslintpnpm exec eslint
.github/workflows/e2e-test.yml timeout-minutes: 10 → 20
.github/workflows/e2e-bucket-test.yml timeout-minutes: 10 → 20
.github/workflows/integration-test.yml timeout-minutes: 10 → 20

Running locally (devpod)

# From repo root — devpod has localhost access to containers
bash scripts/e2e.sh

# Or via lerna (as CI does):
IS_CI_AUTOMATION=yes lerna run test:e2e:ci --scope=send-suite-e2e

The devcontainer is only used with devpod for local testing. GitHub Actions CI uses ghcr.io/.../cached-devcontainer:latest as the container image directly (not the .devcontainer/ configuration).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment