From c27e807c4345841e2715f15a6b96718406588393 Mon Sep 17 00:00:00 2001 From: luciano Date: Wed, 29 Apr 2026 13:52:11 +0200 Subject: [PATCH] test(auth): add unit env tests and E2E sign-up/sign-in flow Unit tests on @ketopath/auth (added to the Vitest workspace): - readAuthEnv accepts a valid configuration - rejects secret < 32 chars and non-URL BETTER_AUTH_URL - preserves Google credentials when both vars are set Playwright e2e/auth.spec.ts: - happy path: sign-up creates a user, redirects home with welcome message, sign-out clears session, sign-in with the same credentials restores it - error path: invalid credentials surface a role="alert" message - each test uses a unique generated email to avoid DB collisions home.spec.ts: links instead of buttons (CTAs are now next/link). Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/web/e2e/auth.spec.ts | 47 +++++++++++++++++++++++++++++++++ apps/web/e2e/home.spec.ts | 4 +-- packages/auth/package.json | 4 ++- packages/auth/src/env.test.ts | 48 ++++++++++++++++++++++++++++++++++ packages/auth/vitest.config.ts | 9 +++++++ vitest.workspace.ts | 2 +- 6 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 apps/web/e2e/auth.spec.ts create mode 100644 packages/auth/src/env.test.ts create mode 100644 packages/auth/vitest.config.ts diff --git a/apps/web/e2e/auth.spec.ts b/apps/web/e2e/auth.spec.ts new file mode 100644 index 0000000..90b6ad4 --- /dev/null +++ b/apps/web/e2e/auth.spec.ts @@ -0,0 +1,47 @@ +import { expect, test } from '@playwright/test'; + +function uniqueEmail(): string { + const ts = Date.now(); + const rand = Math.floor(Math.random() * 1e6); + return `e2e-${ts}-${rand}@ketopath.test`; +} + +test.describe('email/password authentication', () => { + test('sign-up, sign-out and sign-in restore the session', async ({ page }) => { + const email = uniqueEmail(); + const password = 'Password!123'; + const name = 'Utente E2E'; + + // Sign up + await page.goto('/sign-up'); + await page.getByLabel('Nome').fill(name); + await page.getByLabel('Email').fill(email); + await page.getByLabel('Password').fill(password); + await page.getByRole('button', { name: 'Crea account' }).click(); + + await page.waitForURL('/'); + await expect(page.getByText(`Accesso effettuato come ${name}`)).toBeVisible(); + + // Sign out + await page.getByRole('button', { name: 'Esci' }).click(); + await expect(page.getByRole('link', { name: 'Inizia gratis' })).toBeVisible(); + + // Sign in with the same credentials + await page.goto('/sign-in'); + await page.getByLabel('Email').fill(email); + await page.getByLabel('Password').fill(password); + await page.getByRole('button', { name: 'Accedi', exact: true }).click(); + + await page.waitForURL('/'); + await expect(page.getByText(`Accesso effettuato come ${name}`)).toBeVisible(); + }); + + test('shows an error message on invalid credentials', async ({ page }) => { + await page.goto('/sign-in'); + await page.getByLabel('Email').fill('non-esiste@ketopath.test'); + await page.getByLabel('Password').fill('Password!123'); + await page.getByRole('button', { name: 'Accedi', exact: true }).click(); + + await expect(page.getByRole('alert')).toBeVisible(); + }); +}); diff --git a/apps/web/e2e/home.spec.ts b/apps/web/e2e/home.spec.ts index 0ba446b..0cf9d74 100644 --- a/apps/web/e2e/home.spec.ts +++ b/apps/web/e2e/home.spec.ts @@ -5,7 +5,7 @@ test('home page renders Italian copy and CTAs', async ({ page }) => { await expect(page).toHaveTitle(/KetoPath/); await expect(page.getByRole('heading', { name: 'KetoPath' })).toBeVisible(); - await expect(page.getByRole('button', { name: 'Inizia gratis' })).toBeVisible(); - await expect(page.getByRole('button', { name: 'Scopri come funziona' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'Inizia gratis' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'Scopri come funziona' })).toBeVisible(); await expect(page.getByText(/non sostituiscono il parere di un medico/)).toBeVisible(); }); diff --git a/packages/auth/package.json b/packages/auth/package.json index acd5921..f23c170 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -11,7 +11,9 @@ }, "scripts": { "typecheck": "tsc --noEmit", - "lint": "eslint src --max-warnings=0" + "lint": "eslint src --max-warnings=0", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { "@ketopath/db": "workspace:*", diff --git a/packages/auth/src/env.test.ts b/packages/auth/src/env.test.ts new file mode 100644 index 0000000..f41c421 --- /dev/null +++ b/packages/auth/src/env.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from 'vitest'; + +import { readAuthEnv } from './env.js'; + +const VALID_SECRET = 'a'.repeat(32); + +describe('readAuthEnv', () => { + it('accepts a valid configuration', () => { + const env = readAuthEnv({ + BETTER_AUTH_SECRET: VALID_SECRET, + BETTER_AUTH_URL: 'http://localhost:3000', + } as NodeJS.ProcessEnv); + + expect(env.BETTER_AUTH_SECRET).toBe(VALID_SECRET); + expect(env.BETTER_AUTH_URL).toBe('http://localhost:3000'); + expect(env.GOOGLE_CLIENT_ID).toBeUndefined(); + expect(env.GOOGLE_CLIENT_SECRET).toBeUndefined(); + }); + + it('rejects a secret shorter than 32 chars', () => { + expect(() => + readAuthEnv({ + BETTER_AUTH_SECRET: 'troppo-corto', + BETTER_AUTH_URL: 'http://localhost:3000', + } as NodeJS.ProcessEnv), + ).toThrow(/almeno 32/); + }); + + it('rejects a non-URL BETTER_AUTH_URL', () => { + expect(() => + readAuthEnv({ + BETTER_AUTH_SECRET: VALID_SECRET, + BETTER_AUTH_URL: 'not-a-url', + } as NodeJS.ProcessEnv), + ).toThrow(); + }); + + it('preserves Google credentials when provided', () => { + const env = readAuthEnv({ + BETTER_AUTH_SECRET: VALID_SECRET, + BETTER_AUTH_URL: 'http://localhost:3000', + GOOGLE_CLIENT_ID: 'cid', + GOOGLE_CLIENT_SECRET: 'csec', + } as NodeJS.ProcessEnv); + expect(env.GOOGLE_CLIENT_ID).toBe('cid'); + expect(env.GOOGLE_CLIENT_SECRET).toBe('csec'); + }); +}); diff --git a/packages/auth/vitest.config.ts b/packages/auth/vitest.config.ts new file mode 100644 index 0000000..e723c48 --- /dev/null +++ b/packages/auth/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + name: '@ketopath/auth', + environment: 'node', + include: ['src/**/*.test.ts'], + }, +}); diff --git a/vitest.workspace.ts b/vitest.workspace.ts index 9a6c8cb..6d6e98d 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -1,3 +1,3 @@ import { defineWorkspace } from 'vitest/config'; -export default defineWorkspace(['./packages/shared']); +export default defineWorkspace(['./packages/shared', './packages/auth']);