DEV Community

Roberto Luna
Roberto Luna

Posted on

Fixing Broken E2E Authentication in Craveview’s Production CI Pipeline

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
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

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_PASS secret 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;
+}
Enter fullscreen mode Exit fullscreen mode

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();
  });
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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_PASS nightly 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)