Files
lunarfront-app/packages/backend/api-tests/run.ts
Ryan Moon 9400828f62 Rename Forte to LunarFront, generalize for any small business
Rebrand from Forte (music-store-specific) to LunarFront (any small business):
- Package namespace @forte/* → @lunarfront/*
- Database forte/forte_test → lunarfront/lunarfront_test
- Docker containers, volumes, connection strings
- UI branding, localStorage keys, test emails
- All documentation and planning docs

Generalize music-specific terminology:
- instrumentDescription → itemDescription
- instrumentCount → itemCount
- instrumentType → itemCategory (on service templates)
- New migration 0027_generalize_terminology for column renames
- Seed data updated with generic examples
- RBAC descriptions updated
2026-03-30 08:51:54 -05:00

252 lines
9.4 KiB
TypeScript

import { spawn, type Subprocess } from 'bun'
import postgres from 'postgres'
import { printHeader, printSummary, type TestResult } from './lib/reporter.js'
import { getSuites, runSuite } from './lib/context.js'
import { createClient } from './lib/client.js'
// --- Config ---
const DB_HOST = process.env.DB_HOST ?? 'localhost'
const DB_PORT = Number(process.env.DB_PORT ?? '5432')
const DB_USER = process.env.DB_USER ?? 'lunarfront'
const DB_PASS = process.env.DB_PASS ?? 'lunarfront'
const TEST_DB = 'lunarfront_api_test'
const TEST_PORT = 8001
const BASE_URL = `http://localhost:${TEST_PORT}`
const COMPANY_ID = 'a0000000-0000-0000-0000-000000000001'
const LOCATION_ID = 'a0000000-0000-0000-0000-000000000002'
// --- Parse CLI args ---
const args = process.argv.slice(2)
let filterSuite: string | undefined
let filterTags: string[] | undefined
for (let i = 0; i < args.length; i++) {
if (args[i] === '--suite' && args[i + 1]) filterSuite = args[++i]
if (args[i] === '--tag' && args[i + 1]) filterTags = args[++i].split(',')
}
// --- DB setup ---
async function setupDatabase() {
const adminSql = postgres(`postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/postgres`)
const [exists] = await adminSql`SELECT 1 FROM pg_database WHERE datname = ${TEST_DB}`
if (!exists) {
await adminSql.unsafe(`CREATE DATABASE ${TEST_DB}`)
console.log(` Created database ${TEST_DB}`)
}
await adminSql.end()
// Run migrations
const { execSync } = await import('child_process')
execSync(`bunx drizzle-kit migrate`, {
cwd: new URL('..', import.meta.url).pathname,
env: {
...process.env,
DATABASE_URL: `postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${TEST_DB}`,
},
stdio: 'pipe',
})
// Truncate all tables
const testSql = postgres(`postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${TEST_DB}`)
await testSql.unsafe(`
DO $$ DECLARE r RECORD;
BEGIN
SET client_min_messages TO WARNING;
FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public') LOOP
EXECUTE 'TRUNCATE TABLE ' || quote_ident(r.tablename) || ' CASCADE';
END LOOP;
RESET client_min_messages;
END $$
`)
// Seed company + location (company table stays as store settings)
await testSql`INSERT INTO company (id, name, timezone) VALUES (${COMPANY_ID}, 'Test Store', 'America/Chicago')`
await testSql`INSERT INTO location (id, name) VALUES (${LOCATION_ID}, 'Test Location')`
// Seed lookup tables
const { SYSTEM_UNIT_STATUSES, SYSTEM_ITEM_CONDITIONS } = await import('../src/db/schema/lookups.js')
for (const s of SYSTEM_UNIT_STATUSES) {
await testSql`INSERT INTO inventory_unit_status (name, slug, description, is_system, sort_order) VALUES (${s.name}, ${s.slug}, ${s.description}, true, ${s.sortOrder})`
}
for (const c of SYSTEM_ITEM_CONDITIONS) {
await testSql`INSERT INTO item_condition (name, slug, description, is_system, sort_order) VALUES (${c.name}, ${c.slug}, ${c.description}, true, ${c.sortOrder})`
}
// Seed RBAC permissions and default roles
const { SYSTEM_PERMISSIONS, DEFAULT_ROLES } = await import('../src/db/seeds/rbac.js')
for (const p of SYSTEM_PERMISSIONS) {
await testSql`INSERT INTO permission (slug, domain, action, description) VALUES (${p.slug}, ${p.domain}, ${p.action}, ${p.description}) ON CONFLICT (slug) DO NOTHING`
}
const permRows = await testSql`SELECT id, slug FROM permission`
const permMap = new Map(permRows.map((r: any) => [r.slug, r.id]))
for (const roleDef of DEFAULT_ROLES) {
const [role] = await testSql`INSERT INTO role (name, slug, description, is_system) VALUES (${roleDef.name}, ${roleDef.slug}, ${roleDef.description}, true) RETURNING id`
for (const permSlug of roleDef.permissions) {
const permId = permMap.get(permSlug)
if (permId) {
await testSql`INSERT INTO role_permission (role_id, permission_id) VALUES (${role.id}, ${permId})`
}
}
}
// Seed default modules
const DEFAULT_MODULES = [
{ slug: 'inventory', name: 'Inventory', description: 'Product catalog, stock tracking, and unit management', enabled: true },
{ slug: 'pos', name: 'Point of Sale', description: 'Sales transactions, cash drawer, and receipts', enabled: true },
{ slug: 'repairs', name: 'Repairs', description: 'Repair ticket management, batches, and service templates', enabled: true },
{ slug: 'rentals', name: 'Rentals', description: 'Rental agreements and billing', enabled: false },
{ slug: 'lessons', name: 'Lessons', description: 'Scheduling, staff management, and billing', enabled: false },
{ slug: 'files', name: 'Files', description: 'Shared file storage with folder organization', enabled: true },
{ slug: 'vault', name: 'Vault', description: 'Encrypted password and secret manager', enabled: true },
{ slug: 'email', name: 'Email', description: 'Email campaigns, templates, and sending', enabled: false },
{ slug: 'reports', name: 'Reports', description: 'Business reports and data export', enabled: true },
]
for (const m of DEFAULT_MODULES) {
await testSql`INSERT INTO module_config (slug, name, description, licensed, enabled) VALUES (${m.slug}, ${m.name}, ${m.description}, true, ${m.enabled}) ON CONFLICT (slug) DO NOTHING`
}
await testSql.end()
console.log(' Database ready')
}
// --- Start backend ---
async function killPort(port: number) {
try {
const { execSync } = await import('child_process')
execSync(`lsof -ti:${port} | xargs kill -9 2>/dev/null || true`, { stdio: 'pipe' })
await new Promise((r) => setTimeout(r, 1000))
} catch {}
}
async function startBackend(): Promise<Subprocess> {
await killPort(TEST_PORT)
const proc = spawn({
cmd: ['bun', 'run', 'src/main.ts'],
cwd: new URL('..', import.meta.url).pathname,
env: {
...process.env,
DATABASE_URL: `postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${TEST_DB}`,
REDIS_URL: process.env.REDIS_URL ?? 'redis://localhost:6379',
JWT_SECRET: 'test-secret-for-api-tests',
PORT: String(TEST_PORT),
HOST: '0.0.0.0',
NODE_ENV: 'development',
LOG_LEVEL: 'error',
STORAGE_LOCAL_PATH: '/tmp/lunarfront-test-files',
},
stdout: 'pipe',
stderr: 'pipe',
})
// Wait for health check
for (let i = 0; i < 30; i++) {
try {
const res = await fetch(`${BASE_URL}/v1/health`)
if (res.ok) {
console.log(` Backend running on port ${TEST_PORT}`)
return proc
}
} catch {}
await new Promise((r) => setTimeout(r, 500))
}
// Check if process died
const exitCode = proc.exitCode
if (exitCode !== null) {
console.error(` Backend exited with code ${exitCode}`)
}
proc.kill()
throw new Error('Backend failed to start within 15 seconds')
}
// --- Register test user ---
async function registerTestUser(): Promise<string> {
const testPassword = 'testpassword1234'
const headers = { 'Content-Type': 'application/json' }
const registerRes = await fetch(`${BASE_URL}/v1/auth/register`, {
method: 'POST',
headers,
body: JSON.stringify({
email: 'test@lunarfront.dev',
password: testPassword,
firstName: 'Test',
lastName: 'Runner',
role: 'admin',
}),
})
const registerData = await registerRes.json() as { token?: string; user?: { id: string } }
// Assign admin role to the user via direct SQL
if (registerRes.status === 201 && registerData.user) {
const assignSql = postgres(`postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${TEST_DB}`)
const [adminRole] = await assignSql`SELECT id FROM role WHERE slug = 'admin' LIMIT 1`
if (adminRole) {
await assignSql`INSERT INTO user_role_assignment (user_id, role_id) VALUES (${registerData.user.id}, ${adminRole.id}) ON CONFLICT DO NOTHING`
}
await assignSql.end()
}
// Login to get token with permissions loaded
const loginRes = await fetch(`${BASE_URL}/v1/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'test@lunarfront.dev', password: testPassword }),
})
const loginData = await loginRes.json() as { token?: string }
if (loginRes.status !== 200 || !loginData.token) throw new Error(`Auth failed: ${JSON.stringify(loginData)}`)
return loginData.token
}
// --- Import suites ---
async function loadSuites() {
const { readdirSync } = await import('fs')
const suitesDir = new URL('./suites/', import.meta.url).pathname
const files = readdirSync(suitesDir).filter((f: string) => f.endsWith('.ts'))
for (const file of files) {
await import(`./suites/${file}`)
}
}
// --- Main ---
async function main() {
let backend: Subprocess | undefined
try {
console.log('\n Setting up...')
await setupDatabase()
backend = await startBackend()
const token = await registerTestUser()
console.log(' Authenticated\n')
await loadSuites()
const suites = getSuites().filter((s) => !filterSuite || s.name.toLowerCase().includes(filterSuite.toLowerCase()))
printHeader(BASE_URL)
const allResults: TestResult[] = []
const startTime = performance.now()
for (const s of suites) {
const results = await runSuite(s, BASE_URL, token, filterTags)
allResults.push(...results)
}
const totalDuration = performance.now() - startTime
printSummary(allResults, totalDuration)
const failed = allResults.some((r) => !r.passed)
process.exit(failed ? 1 : 0)
} catch (err) {
console.error('\n Fatal error:', err)
process.exit(1)
} finally {
if (backend) backend.kill()
}
}
main()