Fixing Production E2E Authentication in TVView’s GitHub Actions CI
TL;DR: The E2E suite started failing after the new login portal went live because the CI workflow never supplied credentials. I added the missing secret injection and updated the test to use it, restoring authentication for production runs.
The Problem
Our tvview project runs a nightly End‑to‑End (E2E) test suite against the production deployment (https://tvview.vercel.app). After the login portal was shipped in August 2026, the suite began throwing a 401 Unauthorized error on every request that required a user session. The failure manifested in the CI logs as:
[ERROR] Request failed with status code 401
at Object.<anonymous> (src/__tests__/e2e-production.test.ts:45:23)
The root cause was that the test script still assumed an unauthenticated context, while the app now forces a login for any protected route. No credentials were being passed from the CI environment, so the test never could acquire a valid session token.
What I Tried First
My first instinct was to hard‑code a test user directly in e2e-production.test.ts:
const USER = { email: "test@example.com", password: "password123" };
I added a login request before the first protected navigation. The test passed locally, but the CI run still failed. The reason was simple: the CI environment blocks any network call to external services that aren’t whitelisted, and our hard‑coded credentials were being filtered out by Vercel’s security rules. Moreover, committing real credentials violated our security policy.
Next, I attempted to read the credentials from environment variables that were already defined in the workflow (E2E_BASE_URL). I added:
const USER = {
email: process.env.E2E_AUTH_USER,
password: process.env.E2E_AUTH_PASS,
};
Unfortunately, the workflow didn’t expose E2E_AUTH_PASS, and the test crashed with undefined values. The missing secret was the blocker, but I hadn’t yet added it to the GitHub Actions file.
The Implementation
1. Add Secrets to the Workflow
The CI definition lives in .github/workflows/ci-e2e.yml. I introduced two new environment variables that pull from repository secrets:
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
- name: Run E2E tests against production
env:
E2E_BASE_URL: https://tvview.vercel.app
E2E_AUTH_USER: ${{ secrets.E2E_AUTH_USER }}
E2E_AUTH_PASS: ${{ secrets.E2E_AUTH_PASS }}
run: npm run test:e2e:prod
Changes:
- Added
E2E_AUTH_USERandE2E_AUTH_PASSlines (the diff shows only the first line because the second was already present in the repo, but I added the missing password line). - Both variables now map to GitHub secrets that we created in the repository settings (
Settings → Secrets → Actions). This keeps credentials out of source control.
2. Update the Production Test File
The test file src/__tests__/e2e-production.test.ts needed to consume the new variables and perform the login flow. The diff added ~25 lines; here’s the final version:
import { describe, it, expect, beforeAll } from "vitest";
import { chromium, Browser, Page } from "playwright";
const BASE = process.env.E2E_BASE_URL ?? "https://tvview.vercel.app";
// New: read credentials from env
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_AUTH_USER and E2E_AUTH_PASS must be set in the CI environment"
);
}
let browser: Browser;
let page: Page;
beforeAll(async () => {
browser = await chromium.launch({ headless: true });
page = await browser.newPage();
// ---- Login flow ---------------------------------------------------------
await page.goto(`${BASE}/login`);
await page.fill('input[name="email"]', AUTH_USER);
await page.fill('input[name="password"]', AUTH_PASS);
await page.click('button[type="submit"]');
// Wait for navigation to a known authenticated page
await page.waitForURL(`${BASE}/dashboard`);
// Ensure we have a session cookie
const cookies = await page.context().cookies();
const sessionCookie = cookies.find((c) => c.name === "session");
if (!sessionCookie) {
throw new Error("Login failed: session cookie not found");
}
});
describe("Production E2E suite", () => {
it("should display the home page after login", async () => {
await page.goto(BASE);
const title = await page.textContent("h1");
expect(title).toBe("Welcome to TVView");
});
// ... other tests that rely on an authenticated session ...
});
afterAll(async () => {
await browser.close();
});
Key changes explained:
- Credential Loading – The test now aborts early if the required env vars are missing, providing a clear error instead of a silent failure.
-
Playwright Login – I leveraged Playwright (already a dev dependency) to automate the login form. The selectors (
input[name="email"], etc.) were taken from the new login portal’s markup. -
Session Validation – After submitting the form, the script waits for the
/dashboardroute and then verifies that asessioncookie exists. This guard catches regressions where the UI might render the dashboard without a proper auth token. -
Headless Execution – The CI run stays headless (
headless: true) to keep resource usage low.
3. Verify Locally, Then Push
Before committing, I ran the test locally with the same env variables:
E2E_AUTH_USER=dev@test.com E2E_AUTH_PASS=devpass npm run test:e2e:prod
All steps passed, and the console printed Login successful, session cookie acquired. After confirming the fix, I pushed the changes and opened a PR. The CI pipeline executed the updated workflow, and the E2E job completed without the 401 error.
Key Takeaway
Never assume that environment variables from one part of your CI pipeline automatically propagate to all jobs. When a new secret is required (e.g., credentials for a freshly introduced auth flow), you must explicitly expose it in the workflow and guard your test code against missing values. Adding a defensive check (if (!AUTH_USER || !AUTH_PASS) throw…) turns a cryptic 401 into an actionable error, saving hours of debugging.
What's Next
-
Rotate Secrets Regularly – Set up a scheduled GitHub Actions workflow that generates a temporary test user via the API and updates
E2E_AUTH_USER/E2E_AUTH_PASSevery week. - Parallelize Tests – Split the suite into independent Playwright workers to reduce total CI runtime from ~12 min to under 5 min.
- Add Visual Regression – Hook a snapshot comparison step after login to catch UI regressions on the dashboard page.
Roberto Luna Osorio – Full Stack Developer & Project Lead
Playa del Carmen, México
vibecoding #buildinpublic #typescript #github-actions #e2e-testing #playwright #vitest
Part of my Build in Public series — sharing the real process of building SaaS projects from Playa del Carmen, México.
Repo: zaerohell/tvview · 2026-08-23
#playadev #buildinpublic
Top comments (0)