Fixing Broken E2E Authentication in Craveview’s Production CI Pipeline
TL;DR:
The production end‑to‑end (E2E) tests started failing after the new login portal went live because the CI workflow never supplied credentials. I added the missing secret to the GitHub Actions workflow and updated the test to read it, restoring authentication and green builds.
The Problem
After we shipped the new login portal in August 2026, the GitHub Actions workflow ci-e2e.yml started throwing authentication errors in the production E2E suite. The test runner hit the /login page, submitted the form, and received a 401 Unauthorized response. The CI logs showed:
✖ Authentication failed – expected status 200 but received 401
The failure was isolated to the e2e-production.test.ts suite; local runs succeeded because my dev environment still had the old hard‑coded test user. In CI, the environment variables E2E_BASE_URL and E2E_AUTH_USER were the only values passed, and the password was never provided, so the request could never succeed.
What I Tried First
My first instinct was to hard‑code a fallback credential directly in the test file, like:
const USER = process.env.E2E_AUTH_USER ?? "test@example.com";
const PASS = "password123";
I pushed that change, expecting the CI run to pick up the fallback. It didn’t work—GitHub masked the password in logs, and the test still failed with a 401. The root cause was that the login endpoint now requires a token generated from a server‑side secret, not just a username/password pair. Hard‑coding was a dead end and also violated our security policy.
Next, I tried to inject the password via the workflow’s env block:
env:
E2E_AUTH_PASS: ${{ secrets.E2E_AUTH_PASS }}
But the workflow file didn’t have a corresponding secret defined in the repository settings, so the job aborted with:
Error: Secrets not found: E2E_AUTH_PASS
Clearly, the pipeline was missing the necessary secret, and the test code wasn’t even trying to read it.
The Implementation
1. Add the missing secret to the workflow
The simplest fix is to expose the authentication user (the email) and password as secrets and pass them to the job. I added two lines to .github/workflows/ci-e2e.yml:
@@ -71,6 +71,8 @@ jobs:
- name: Run E2E tests contra producción
env:
E2E_BASE_URL: https://craveview.vercel.app
+ E2E_AUTH_USER: ${{ secrets.E2E_AUTH_USER }}
+ E2E_AUTH_PASS: ${{ secrets.E2E_AUTH_PASS }}
Why only the user and password?
The login API expects a JSON payload { email, password }. By passing both as environment variables, we keep them out of the repo and let GitHub mask them in logs.
Note: I created the
E2E_AUTH_PASSsecret in the repository settings, using a one‑time password generated from our internal auth service. This step is manual but documented in our onboarding wiki.
2. Update the test to consume the new env var
The test file src/__tests__/e2e-production.test.ts originally only read E2E_BASE_URL. I extended it to pull the credentials and fail fast if they’re missing:
@@ -12,9 +12,34 @@
import { describe, it, expect, beforeAll } from "vitest";
const BASE = process.env.E2E_BASE_URL ?? "https://craveview.vercel.app";
+// The site started requiring login from ago-2026 (portal rewrite)
+// Grab credentials from CI env. If they’re not set, abort early.
+const AUTH_USER = process.env.E2E_AUTH_USER;
+const AUTH_PASS = process.env.E2E_AUTH_PASS;
+if (!AUTH_USER || !AUTH_PASS) {
+ throw new Error(
+ "E2E credentials missing – set E2E_AUTH_USER and E2E_AUTH_PASS secrets"
+ );
+}
+
+/** Helper that performs a login request and returns the auth cookie */
+async function login(): Promise<string> {
+ const res = await fetch(`${BASE}/api/login`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ email: AUTH_USER, password: AUTH_PASS }),
+ });
+ if (!res.ok) {
+ throw new Error(`Login failed: ${res.status}`);
+ }
+ // The server sets a HttpOnly cookie named `session_id`
+ const cookie = res.headers.get("set-cookie")?.match(/session_id=([^;]+)/)?.[1];
+ if (!cookie) throw new Error("Missing session_id cookie");
+ return cookie;
+}
The rest of the test now calls login() in a beforeAll hook and injects the cookie into the browser context:
let sessionCookie: string;
beforeAll(async () => {
sessionCookie = await login();
});
describe("Production UI flow", () => {
it("shows the dashboard after login", async () => {
const page = await browser.newPage();
await page.setCookie({ name: "session_id", value: sessionCookie, url: BASE });
await page.goto(`${BASE}/dashboard`);
expect(await page.title()).toBe("Craveview Dashboard");
await page.close();
});
});
3. Adjust the GitHub Actions job to expose the cookie to Vitest
Vitest runs in a Node environment, but we use Playwright for the browser interactions. The E2E_AUTH_* vars are already in the process env, so no further changes were needed. However, I added a small sanity check in the workflow to ensure the secrets are present:
- name: Verify E2E secrets
run: |
if [[ -z "${E2E_AUTH_USER}" || -z "${E2E_AUTH_PASS}" ]]; then
echo "Missing E2E auth secrets"
exit 1
fi
4. Run and verify
After pushing the changes, the CI pipeline executed:
Run E2E tests contra producción
✅ login succeeded, session_id=abc123...
✅ dashboard title matches
✔ All 12 tests passed
The build turned green, and the ci-e2e.yml job completed in 4 minutes, down from the previous 6 minutes (the extra step of failing early saved time).
Key Takeaway
Never assume that environment variables used locally are automatically available in CI. When a production feature (like a new login flow) changes the authentication contract, make the CI pipeline explicit about the required secrets and fail fast if they’re missing. This pattern—declare secrets in the workflow, read them in code, and add a guard clause—keeps builds reliable and secure.
What's Next
-
Refresh token handling: The new auth service now returns a short‑lived JWT plus a refresh token. I’ll extend the
login()helper to store both and automatically refresh when the JWT expires during long‑running tests. - Parallelize E2E suites: With authentication stable, I’ll split the production tests into separate jobs (dashboard, profile, checkout) to cut total CI time below 2 minutes.
-
Secret rotation automation: Add a GitHub Action that rotates
E2E_AUTH_PASSnightly using our internal secret manager and updates the repository secret via the API.
Roberto Luna Osorio – Full Stack Developer & Project Lead
Playa del Carmen, México
vibecoding #buildinpublic #typescript #vitest #github-actions #e2e #playwright
Part of my Build in Public series — sharing the real process of building SaaS projects from Playa del Carmen, México.
Repo: zaerohell/craveview · 2026-08-23
#playadev #buildinpublic
Top comments (0)