Add user auth with JWT, switch to bun test

- User table with company_id FK, unique email, role enum
- Register/login routes with bcrypt + JWT token generation
- Auth plugin with authenticate decorator and role guards
- Login uses globally unique email (no company header needed)
- Dev-auth plugin kept as fallback when JWT_SECRET not set
- Switched from vitest to bun:test (vitest had ESM resolution
  issues with zod in Bun's module structure)
- Upgraded to zod 4
- Added Dockerfile.dev and API service to docker-compose
- 8 tests passing (health + auth)
This commit is contained in:
Ryan Moon
2026-03-27 17:33:05 -05:00
parent c1cddd6b74
commit 979a9a2c00
28 changed files with 1181 additions and 39 deletions

View File

@@ -1,10 +1,13 @@
import type { FastifyInstance } from 'fastify'
import { buildApp } from '../main.js'
import { sql } from 'drizzle-orm'
import { companies, locations } from '../db/schema/stores.js'
export const TEST_COMPANY_ID = '00000000-0000-0000-0000-000000000099'
export const TEST_LOCATION_ID = '00000000-0000-0000-0000-000000000099'
/**
* Build a fresh Fastify app instance for testing.
* Each test gets its own app — no shared state.
*/
export async function createTestApp(): Promise<FastifyInstance> {
const app = await buildApp()
@@ -14,7 +17,6 @@ export async function createTestApp(): Promise<FastifyInstance> {
/**
* Truncate all tables in the test database.
* Call this in beforeEach to guarantee a clean slate per test.
*/
export async function cleanDb(app: FastifyInstance): Promise<void> {
await app.db.execute(sql`
@@ -27,3 +29,49 @@ export async function cleanDb(app: FastifyInstance): Promise<void> {
END $$
`)
}
/**
* Seed a test company and location. Call after cleanDb.
*/
export async function seedTestCompany(app: FastifyInstance): Promise<void> {
await app.db.insert(companies).values({
id: TEST_COMPANY_ID,
name: 'Test Music Co.',
timezone: 'America/Chicago',
})
await app.db.insert(locations).values({
id: TEST_LOCATION_ID,
companyId: TEST_COMPANY_ID,
name: 'Test Location',
})
}
/**
* Register a user and return the JWT token.
*/
export async function registerAndLogin(
app: FastifyInstance,
overrides: {
email?: string
password?: string
firstName?: string
lastName?: string
role?: string
} = {},
): Promise<{ token: string; user: Record<string, unknown> }> {
const response = await app.inject({
method: 'POST',
url: '/v1/auth/register',
headers: { 'x-company-id': TEST_COMPANY_ID },
payload: {
email: overrides.email ?? 'test@forte.dev',
password: overrides.password ?? 'testpassword123',
firstName: overrides.firstName ?? 'Test',
lastName: overrides.lastName ?? 'User',
role: overrides.role ?? 'admin',
},
})
const body = response.json()
return { token: body.token, user: body.user }
}

View File

@@ -8,6 +8,7 @@ const TEST_DB_URL =
process.env.DATABASE_URL = TEST_DB_URL
process.env.NODE_ENV = 'test'
process.env.LOG_LEVEL = 'silent'
process.env.JWT_SECRET = 'test-secret-for-jwt-signing'
/**
* Ensure the forte_test database exists before tests run.