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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ NEXT_PUBLIC_SITE_URL=http://localhost:3000

# Server-only OpenAI credential. Never prefix this variable with NEXT_PUBLIC_.
NEXT_APP_OPENAI_API_KEY=your-openai-api-key

# Optional Playwright credentials for authenticated protected-flow E2E tests.
# Use a dedicated non-production test user and never commit real values.
E2E_AUTH_EMAIL=e2e-user@example.com
E2E_AUTH_PASSWORD=replace-with-a-dedicated-test-password
2 changes: 2 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ jobs:
NEXT_PUBLIC_SUPABASE_ANON_KEY: test-anon-key
NEXT_PUBLIC_SITE_URL: http://127.0.0.1:3000
NEXT_APP_OPENAI_API_KEY: test-openai-key
E2E_AUTH_EMAIL: ${{ secrets.E2E_AUTH_EMAIL }}
E2E_AUTH_PASSWORD: ${{ secrets.E2E_AUTH_PASSWORD }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# testing
/coverage
/.test-dist
/playwright/.auth/
/playwright-report/
/test-results/

Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ The versioned Supabase schema in `supabase/migrations` defines the application t
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Browser and server | Supabase anonymous key; requires appropriate Row Level Security |
| `NEXT_PUBLIC_SITE_URL` | Browser and server | Application origin used for authentication redirects |
| `NEXT_APP_OPENAI_API_KEY` | Server only | OpenAI credential used to generate workouts and meal plans |
| `E2E_AUTH_EMAIL` | Local/CI test runner only | Optional dedicated Supabase test-user email for authenticated Playwright flows |
| `E2E_AUTH_PASSWORD` | Local/CI test runner only | Optional dedicated Supabase test-user password for authenticated Playwright flows |

Never commit `.env.local` or real credentials. The checked-in `.env.example` contains placeholders only.

Expand Down Expand Up @@ -117,6 +119,8 @@ npm test

The test command compiles the selected TypeScript source and tests into the ignored `.test-dist` directory, then runs them with Node's built-in test runner. The baseline suite covers authentication and profile schemas, TDEE calculations, generated workout and meal-plan validation, Supabase client contracts, and the required migration contract.

Playwright always runs the public authentication journeys. Authenticated protected-flow tests are included automatically only when both `E2E_AUTH_EMAIL` and `E2E_AUTH_PASSWORD` are set. Use a dedicated non-production Supabase user, keep those values in `.env.local` or CI secrets, and never commit real credentials. The authenticated setup stores browser state under the ignored `playwright/.auth/` directory.

## Available Scripts

| Command | Description |
Expand All @@ -128,9 +132,9 @@ The test command compiles the selected TypeScript source and tests into the igno
| `npm run typecheck` | Run the TypeScript compiler without emitting files |
| `npm test` | Compile and run the baseline unit tests |
| `npm run check` | Run lint, type-checking, and unit tests |
| `npm run test:e2e` | Run Playwright public authentication journeys in Chromium |
| `npm run test:e2e` | Run Playwright public journeys and, when E2E credentials are configured, authenticated protected-flow checks in Chromium |
| `npm run ci` | Run checks, production build, and end-to-end tests |

## Current Productionization Status

This repository is being hardened incrementally. The current baseline includes deterministic builds, explicit quality scripts, validated authentication recovery, profile and TDEE domains, user-local daily tracking, versioned Supabase schema and Row Level Security policies, and documented environment setup. AI-generated workout and meal-plan output is now validated before persistence and regeneration is non-destructive. Public authentication journeys now have Playwright end-to-end coverage and run in CI. Upcoming work should add authenticated Supabase E2E fixtures, introduce production observability, and automate deployment controls.
This repository is being hardened incrementally. The current baseline includes deterministic builds, explicit quality scripts, validated authentication recovery, profile and TDEE domains, user-local daily tracking, versioned Supabase schema and Row Level Security policies, and documented environment setup. AI-generated workout and meal-plan output is now validated before persistence and regeneration is non-destructive. Public authentication journeys run in CI, and authenticated Supabase protected-flow smoke tests run whenever dedicated E2E credentials are configured. Upcoming work should introduce production observability, mocked AI generation E2E coverage, and automated deployment controls.
25 changes: 25 additions & 0 deletions e2e/auth.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { mkdirSync } from 'node:fs'
import { dirname } from 'node:path'

import { expect, test } from '@playwright/test'

const authFile = 'playwright/.auth/user.json'
const email = process.env.E2E_AUTH_EMAIL
const password = process.env.E2E_AUTH_PASSWORD

test('authenticate as the dedicated E2E user', async ({ page }) => {
if (!email || !password) {
throw new Error('E2E_AUTH_EMAIL and E2E_AUTH_PASSWORD are required for authenticated E2E setup.')
}

await page.goto('/login')
await page.getByLabel('Email address').fill(email)
await page.getByLabel('Password', { exact: true }).fill(password)
await page.getByRole('button', { name: 'Log in' }).click()

await expect(page).toHaveURL(/\/protected$/)
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible()

mkdirSync(dirname(authFile), { recursive: true })
await page.context().storageState({ path: authFile })
})
24 changes: 24 additions & 0 deletions e2e/protected-flow.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { expect, test } from '@playwright/test'

test.describe('authenticated protected flows', () => {
test('loads the protected dashboard with authenticated storage', async ({ page }) => {
await page.goto('/protected')

await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible()
await expect(page.getByRole('button', { name: 'Overview' })).toBeVisible()
await expect(page.getByRole('button', { name: 'Workouts' })).toBeVisible()
await expect(page.getByRole('button', { name: 'Progress' })).toBeVisible()
await expect(page.getByRole('link', { name: 'Profile' }).first()).toHaveAttribute('href', '/protected/profile')
})

test('opens the protected profile setup form', async ({ page }) => {
await page.goto('/protected/profile')

await expect(page.getByRole('heading', { name: 'Set Up Your Profile' })).toBeVisible()
await expect(page.getByLabel('Age')).toBeVisible()
await expect(page.getByLabel('Weight (kg)')).toBeVisible()
await expect(page.getByLabel('Height (cm)')).toBeVisible()
await expect(page.getByLabel('Activity Level')).toBeVisible()
await expect(page.getByRole('button', { name: 'Save Profile' })).toBeVisible()
})
})
32 changes: 28 additions & 4 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,33 @@
import { defineConfig, devices } from '@playwright/test'
import { defineConfig, devices, type PlaywrightTestConfig } from '@playwright/test'

const port = Number(process.env.PORT ?? 3000)
const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? `http://127.0.0.1:${port}`
const authFile = 'playwright/.auth/user.json'
const hasAuthenticatedE2ECredentials = Boolean(process.env.E2E_AUTH_EMAIL && process.env.E2E_AUTH_PASSWORD)

const projects: PlaywrightTestConfig['projects'] = [
{
name: 'public-auth',
testMatch: /public-auth\.spec\.ts/,
use: { ...devices['Desktop Chrome'] },
},
]

if (hasAuthenticatedE2ECredentials) {
projects.push(
{
name: 'authenticated setup',
testMatch: /auth\.setup\.ts/,
use: { ...devices['Desktop Chrome'] },
},
{
name: 'authenticated',
testMatch: /protected-flow\.spec\.ts/,
dependencies: ['authenticated setup'],
use: { ...devices['Desktop Chrome'], storageState: authFile },
},
)
}

export default defineConfig({
testDir: './e2e',
Expand All @@ -16,9 +42,7 @@ export default defineConfig({
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
],
projects,
webServer: process.env.PLAYWRIGHT_BASE_URL ? undefined : {
command: `npm run dev -- --hostname 127.0.0.1 --port ${port}`,
url: baseURL,
Expand Down
16 changes: 16 additions & 0 deletions tests/e2e-harness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ const packageJson = JSON.parse(readFileSync('package.json', 'utf8'))
const workflow = readFileSync('.github/workflows/quality.yml', 'utf8')
const playwrightConfig = readFileSync('playwright.config.ts', 'utf8')
const publicAuthSpec = readFileSync('e2e/public-auth.spec.ts', 'utf8')
const authSetup = readFileSync('e2e/auth.setup.ts', 'utf8')
const protectedFlowSpec = readFileSync('e2e/protected-flow.spec.ts', 'utf8')
const middlewareHelper = readFileSync('utils/supabase/middleware.ts', 'utf8')
const middleware = readFileSync('middleware.ts', 'utf8')
const gitignore = readFileSync('.gitignore', 'utf8')

describe('end-to-end harness contract', () => {
it('defines deterministic Playwright scripts and failure artifacts', () => {
Expand All @@ -30,6 +33,19 @@ describe('end-to-end harness contract', () => {
assert.match(publicAuthSpec, /getByRole\('alert'\)\.filter\(\{ hasText:/)
})

it('adds authenticated protected-flow coverage only when E2E credentials are configured', () => {
assert.match(playwrightConfig, /hasAuthenticatedE2ECredentials/)
assert.match(playwrightConfig, /E2E_AUTH_EMAIL/)
assert.match(playwrightConfig, /E2E_AUTH_PASSWORD/)
assert.match(playwrightConfig, /name: 'authenticated setup'/)
assert.match(playwrightConfig, /name: 'authenticated'/)
assert.match(playwrightConfig, /storageState: authFile/)
assert.match(authSetup, /storageState\(\{ path: authFile \}\)/)
assert.match(protectedFlowSpec, /page\.goto\('\/protected'\)/)
assert.match(protectedFlowSpec, /page\.goto\('\/protected\/profile'\)/)
assert.match(gitignore, /\/playwright\/\.auth\//)
})

it('runs checks, build, and Playwright in CI and uploads failures', () => {
assert.match(workflow, /npm run check/)
assert.match(workflow, /npm run build/)
Expand Down
Loading