Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Install Playwright Chromium
run: pnpm exec playwright install --with-deps chromium

- name: Run lint
run: pnpm lint

Expand All @@ -54,6 +57,8 @@ jobs:
# erroring at startup.
NEXTAUTH_SECRET: ci-dummy-secret
JWT_SECRET: ci-dummy-secret
BETTER_AUTH_SECRET: ci-e2e-only-secret-012345678901234567890123
BETTER_AUTH_URL: http://localhost:3000
MYSQL_HOST: localhost
MYSQL_DATABASE: ci
MYSQL_USER: ci
Expand Down Expand Up @@ -85,6 +90,23 @@ jobs:
exit 1
}

- name: Seed the disposable CI database
env:
MYSQL_HOST: 127.0.0.1
MYSQL_PORT: 3307
MYSQL_DATABASE: ci
MYSQL_USER: ci
MYSQL_PASSWORD: ci
run: |
pnpm e2e:seed
pnpm e2e:seed

- name: Run Playwright end-to-end tests
env:
CI: true
E2E_BASE_URL: http://localhost:3000
run: pnpm e2e

- name: Clean up
if: always()
env:
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public/swe-worker-*.js.map

# testing
/coverage
/playwright-report/
/test-results/

# next.js
/.next/
Expand Down
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,47 @@ with Google can add a password and passkeys from the Account page. New password
registrations require email verification, and Resend is also used for password
resets.

### Browser end-to-end test

The Playwright login test uses a disposable local MySQL database and the local
app. Create an empty database named `va_stats_e2e` and a local database user
with access to it. Set the values below in the shell where you run the commands;
do not point these values at staging or production:

```bash
export MYSQL_HOST=127.0.0.1
export MYSQL_PORT=3306
export MYSQL_DATABASE=va_stats_e2e
export MYSQL_USER=your_local_user
export MYSQL_PASSWORD=your_local_password
export DATABASE_URL=mysql://your_local_user:your_local_password@127.0.0.1:3306/va_stats_e2e
export BETTER_AUTH_SECRET=e2e-local-only-secret-at-least-32-chars
export BETTER_AUTH_URL=http://localhost:3000
```

Apply the Prisma migrations, seed the test records, and start the app in one
terminal:

```bash
pnpm prisma migrate deploy
pnpm e2e:seed
pnpm dev
```

In another terminal, install the browser once and run the login test:

```bash
pnpm exec playwright install chromium
E2E_BASE_URL=http://localhost:3000 pnpm e2e
```

The seed is idempotent and only accepts loopback MySQL hosts with database
names `ci`, `*_e2e`, or `*_test`. It creates one user per staff role, a course,
a batch, and a student assigned to that batch. The test signs in as the seeded
administrator using `e2e-administrator@va-stats.test` and password
`E2E-only-passphrase-2026`. These credentials are for disposable test databases
only.

### One image, two origins

`NEXT_PUBLIC_BASE_URL` differs per environment, and Next.js normally inlines
Expand Down
11 changes: 11 additions & 0 deletions docker-compose.ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ services:
# Disposable MySQL used only by CI and the published-image smoke test.
db:
image: mysql:8.4
ports:
- "127.0.0.1:3307:3306"
cap_add:
- SYS_NICE
environment:
Expand All @@ -17,6 +19,15 @@ services:
start_period: 20s

va-stats-app:
ports:
- "127.0.0.1:3000:3000"
healthcheck:
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1"]
interval: 5s
timeout: 5s
retries: 20
start_period: 60s
start_interval: 3s
environment:
MYSQL_HOST: db
MYSQL_PORT: 3306
Expand Down
2 changes: 2 additions & 0 deletions e2e/credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const E2E_LOGIN_EMAIL = "e2e-administrator@va-stats.test";
export const E2E_LOGIN_PASSWORD = "E2E-only-passphrase-2026";
17 changes: 17 additions & 0 deletions e2e/tests/login.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { expect, test } from "@playwright/test";
import { E2E_LOGIN_EMAIL, E2E_LOGIN_PASSWORD } from "../credentials";

test("staff can sign in with email and password", async ({ page }) => {
await page.goto("/auth/sign-in");
await page.getByLabel("Email").fill(E2E_LOGIN_EMAIL);
await page.getByLabel("Password").fill(E2E_LOGIN_PASSWORD);
await page.getByRole("button", { name: "Sign in", exact: true }).click();

await expect(page).toHaveURL(/\/default$/);
const session = await page.evaluate(async () => {
const response = await fetch("/api/auth/get-session");
return response.json();
});
expect(session.user.email).toBe(E2E_LOGIN_EMAIL);
expect(session.user.role).toBe("ADMINISTRATOR");
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-09-22
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
## Context

See proposal.md for the motivation. The existing pull request workflow runs Node checks, then builds and starts the production Docker image with `docker-compose.ci.yml`, which provides disposable MySQL but does not publish the database or app ports to the runner. Prisma is configured from `MYSQL_*` environment variables. Better Auth requires verified email/password accounts and only admits email addresses found in `vausers`; the staff form currently offers six role values.

## Goals / Non-Goals

**Goals:**

- Reuse the CI image and disposable database already started by the workflow.
- Let the GitHub Actions runner seed that database and drive the app from a real browser.
- Make local runs reproducible with the same seed data and command shape.
- Keep fixture writes repeatable and constrained to a safe database target.

**Non-Goals:**

- Test Google OAuth, passkeys, registration, or password reset in this first browser test.
- Add production seed data or alter production authentication behavior.
- Cover application pages beyond the successful login destination.

## Decisions

1. **Run Playwright on the CI runner against the pull request's production app container.** Publish the app and disposable MySQL ports only on the loopback interface in the CI overlay. This exercises the image CI already builds and checks. Running a separate Next.js development server would test a different runtime; putting browsers in the application image would enlarge the deployed image and couple test tooling to production.

2. **Use one deterministic, idempotent Prisma seed fixture.** Use stable test-only emails and fixture identifiers, including the six role values in the staff form. Link the single batch to the seeded course and the single student through the existing student-to-batch relation. Re-running updates or reuses only fixture-owned rows; it never resets tables or removes unrelated data.

3. **Create the login account as already verified with a Better Auth-compatible password hash.** The seed script should use the supported Better Auth password hashing facility rather than implementing hashing itself, and create the Better Auth user/account records tied to the selected `vausers` email. This avoids email delivery and exercises the same credential format checked by the sign-in endpoint.

4. **Guard seed writes by database target.** The seed operation should require a local host or explicit test database marker, reject production mode and known production hosts, and use a clear opt-in override only where local container networking requires it. CI points it at the published loopback port of the disposable compose database. It should not issue broad cleanup or truncate commands.

5. **Expose a single documented end-to-end command and configure a headless Chromium project.** Keep the browser test separate from the existing Vitest unit suite so CI can run both and report failures clearly. Use an environment-provided base URL, defaulting to the local app URL.

## Risks / Trade-offs

- **A fixed local test password is not suitable for real accounts** → Restrict fixture identity creation to test databases and document that the credentials are disposable.
- **Schema constraints may make minimal fixture rows invalid** → Populate required fields and enum values from the Prisma schema, and keep this work in the seed implementation task.
- **Loopback port collisions on local machines** → Allow the local base URL and MySQL port to be configured while keeping CI's published ports explicit.
- **Browser installation increases CI time** → Install only Chromium and its required system dependencies.

## Migration Plan

No production migration is required. Add test dependencies and configuration, seed the disposable MySQL database after the app's migrations have completed, then run Playwright against the healthy app. CI cleanup must continue to remove the compose project and its volumes on both success and failure.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## Why

The project has unit tests but no browser-level coverage for authentication. A repeatable local fixture and an automated login journey will catch integration failures across Better Auth, Prisma, the running app, and the browser before changes are merged.

## What Changes

- Add Playwright as the browser end-to-end test runner and a documented command for running the login journey locally.
- Add an idempotent seed script that creates one staff user for each role offered by the staff form, one course, one batch for that course, and one student enrolled in that batch. Provide a verified password credential for the login fixture without sending email.
- Run the seeded login end-to-end test in pull request CI against the already-built app and its disposable local MySQL service.
- Keep the seed and CI fixture isolated from production data and safe to run repeatedly.

## Capabilities

### New Capabilities

- `e2e-testing`: Defines the seeded test data, login journey, and local/CI execution requirements for browser end-to-end tests.

### Modified Capabilities

None.

## Impact

- Adds Playwright and browser test configuration, test sources, and package scripts.
- Adds a Prisma-backed development/CI seed script for `vausers`, Better Auth credentials, `vacourses`, `vabatches`, `vastudents`, and `vastudent_to_batch`.
- Updates `.github/workflows/ci.yaml` and the CI Docker Compose overlay so the runner can reach the disposable database and app.
- Does not change production authentication behavior or application data models.
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
## Purpose

Provides a repeatable, isolated browser test environment for exercising the application's real authentication flow against a seeded database.

## ADDED Requirements

### Requirement: Seeded end-to-end fixture

The project MUST provide a repeatable seed operation that creates or updates one active staff user for each role offered by the staff form: `STAFF`, `MANAGEMENT`, `ADMINISTRATOR`, `TELECALLER`, `TRAINER`, and `TRAINERPLUSTELECALLER`. It MUST also create or update one course, one batch referring to that course, one student, and the student's assignment to that batch. The fixture MUST include a verified Better Auth email/password identity for the designated login user and MUST NOT require sending email.

#### Scenario: Seed an empty test database

- **WHEN** the seed operation runs against an empty permitted test database
- **THEN** the database contains one fixture staff user for each listed role, one course, one batch for that course, one student assigned to that batch, and a verified login credential for the designated fixture user

#### Scenario: Repeat the seed operation

- **WHEN** the seed operation runs more than once against the same database
- **THEN** it updates or reuses its fixture records without creating duplicate fixture users, courses, batches, students, assignments, or login credentials

### Requirement: Isolated seeded data

The seed operation MUST be limited to an explicitly local or disposable test database, MUST identify its fixture records deterministically, and MUST NOT delete unrelated records. The local end-to-end instructions MUST state the fixture login credentials and database prerequisites.

#### Scenario: Refuse an unsafe database target

- **WHEN** the seed operation is configured with a production or otherwise non-permitted database target
- **THEN** it exits with a clear error before writing data

#### Scenario: Preserve unrelated database records

- **WHEN** the seed operation runs in a database that contains unrelated records
- **THEN** those records remain unchanged

### Requirement: Browser login coverage

The project MUST provide a browser end-to-end test that enters the seeded user's email and password through the sign-in page and verifies successful authentication and arrival at the authenticated `/default` page.

#### Scenario: Successful email and password login

- **WHEN** the end-to-end test opens the sign-in page and submits the designated fixture user's valid credentials
- **THEN** the browser reaches `/default` with an authenticated session for that fixture user

### Requirement: Local and CI execution

The end-to-end test MUST be runnable through a documented project command against a local application and local MySQL database. Pull request CI MUST seed its disposable MySQL database, run the test against the application built for that pull request, and fail the check if seeding, startup, or the login test fails.

#### Scenario: Run end-to-end coverage in pull request CI

- **WHEN** a pull request runs the CI workflow
- **THEN** CI uses its disposable MySQL database and pull request app build to seed and execute the browser login test before reporting success

#### Scenario: Run end-to-end coverage locally

- **WHEN** a developer follows the documented local command with the test database and application available
- **THEN** the fixture is seeded and the browser login test runs against that local application
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
## 1. Seeded Test Fixture

- [x] 1.1 Add an idempotent Prisma seed script for the six staff roles, course, related batch, student assignment, and verified Better Auth login identity; verify it creates the expected records on an empty test database.
- [x] 1.2 Add database-target safety checks and stable fixture identifiers; verify a second seed run creates no duplicates and an unsafe target is rejected before writes.

## 2. Playwright Login Test

- [x] 2.1 Add Playwright, Chromium configuration, and a separate end-to-end package command; verify Playwright discovers the browser test.
- [x] 2.2 Implement the email/password login test using the seeded identity and assert the authenticated `/default` destination; verify it passes against a locally running app and seeded local MySQL database.
- [x] 2.3 Document local prerequisites, the seed and test commands, and disposable fixture credentials; verify a developer can follow the documented sequence.

## 3. Pull Request CI Integration

- [x] 3.1 Publish the disposable CI MySQL and app ports on loopback in the CI Compose overlay; verify the runner can connect to both services while the compose app remains healthy.
- [x] 3.2 Install Chromium in the CI runner, seed the disposable database, and run the Playwright login test against the PR app; verify the CI workflow fails when seeding or the browser test fails.
- [x] 3.3 Verify the workflow's always-run cleanup removes the compose project and disposable database volumes after both successful and failed end-to-end runs.
58 changes: 58 additions & 0 deletions openspec/specs/e2e-testing/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# E2E Testing Specification

## Purpose

Provides a repeatable, isolated browser test environment for exercising the application's real authentication flow against a seeded database.

## Requirements

### Requirement: Seeded end-to-end fixture

The project MUST provide a repeatable seed operation that creates or updates one active staff user for each role offered by the staff form: `STAFF`, `MANAGEMENT`, `ADMINISTRATOR`, `TELECALLER`, `TRAINER`, and `TRAINERPLUSTELECALLER`. It MUST also create or update one course, one batch referring to that course, one student, and the student's assignment to that batch. The fixture MUST include a verified email/password identity for the designated login user and MUST NOT require sending email.

#### Scenario: Seed an empty test database

- **WHEN** the seed operation runs against an empty permitted test database
- **THEN** the database contains one fixture staff user for each listed role, one course, one batch for that course, one student assigned to that batch, and a verified login credential for the designated fixture user

#### Scenario: Repeat the seed operation

- **WHEN** the seed operation runs more than once against the same database
- **THEN** it updates or reuses its fixture records without creating duplicate fixture users, courses, batches, students, assignments, or login credentials

### Requirement: Isolated seeded data

The seed operation MUST be limited to an explicitly local or disposable test database, MUST identify its fixture records deterministically, and MUST NOT delete unrelated records. The local end-to-end instructions MUST state the fixture login credentials and database prerequisites.

#### Scenario: Refuse an unsafe database target

- **WHEN** the seed operation is configured with a production or otherwise non-permitted database target
- **THEN** it exits with a clear error before writing data

#### Scenario: Preserve unrelated database records

- **WHEN** the seed operation runs in a database that contains unrelated records
- **THEN** those records remain unchanged

### Requirement: Browser login coverage

The project MUST provide a browser end-to-end test that enters the seeded user's email and password through the sign-in page and verifies successful authentication and arrival at the authenticated `/default` page.

#### Scenario: Successful email and password login

- **WHEN** the end-to-end test opens the sign-in page and submits the designated fixture user's valid credentials
- **THEN** the browser reaches `/default` with an authenticated session for that fixture user

### Requirement: Local and CI execution

The end-to-end test MUST be runnable through a documented project command against a local application and local MySQL database. Pull request CI MUST seed its disposable MySQL database, run the test against the application built for that pull request, and fail the check if seeding, startup, or the login test fails.

#### Scenario: Run end-to-end coverage in pull request CI

- **WHEN** a pull request runs the CI workflow
- **THEN** CI uses its disposable MySQL database and pull request app build to seed and execute the browser login test before reporting success

#### Scenario: Run end-to-end coverage locally

- **WHEN** a developer follows the documented local command with the test database and application available
- **THEN** the fixture is seeded and the browser login test runs against that local application
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"e2e:seed": "prisma db seed",
"e2e": "playwright test",
"prepare": "husky",
"postinstall": "prisma generate"
},
Expand Down Expand Up @@ -51,6 +53,7 @@
"styled-components": "6.1.19"
},
"devDependencies": {
"@playwright/test": "^1.63.0",
"@serwist/cli": "9.5.12",
"@serwist/next": "9.5.12",
"@testing-library/jest-dom": "^6.9.1",
Expand Down
18 changes: 18 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
testDir: "./e2e/tests",
fullyParallel: false,
reporter: "list",
retries: process.env.CI ? 1 : 0,
use: {
baseURL: process.env.E2E_BASE_URL ?? "http://localhost:3000",
trace: "retain-on-failure",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
});
Loading
Loading