import type { FastifyInstance } from 'fastify'; import { afterAll, beforeAll, beforeEach } from 'vitest'; import type { AuthResponse } from '@musicshare/shared'; import { buildApp } from '../src/app'; import { sql } from '../src/db'; import { env } from '../src/env'; import { closeConnections } from '../src/lib/lifecycle'; import { redis } from '../src/lib/redis'; function assertTestTargets() { // These wipe data: make it impossible to point them at a dev or prod database. if (!new URL(env.DATABASE_URL).pathname.endsWith('_test')) throw new Error('DATABASE_URL is not a *_test database'); if (new URL(env.REDIS_URL).pathname.replace('/', '') === '' || new URL(env.REDIS_URL).pathname === '/0') { throw new Error('REDIS_URL must select a non-default DB index for tests (e.g. redis://host:6379/1)'); } } export async function resetState() { assertTestTargets(); await sql`truncate users, tracks, track_links, sessions, follows, connections, now_playing, featured_songs, listens, devices, blocks, reports restart identity cascade`; await redis.flushdb(); } /** * Per-file setup: a fresh app (rate limits off unless asked) and clean state * before each test. Returns a getter because the app is built in beforeAll. */ export function useTestApp(opts: { rateLimits?: boolean } = {}) { let app: FastifyInstance; beforeAll(async () => { app = await buildApp({ logger: false, rateLimits: opts.rateLimits ?? false }); await app.ready(); }); beforeEach(resetState); afterAll(async () => { await app.close(); await closeConnections(); }); return () => app; } export const bearer = (token: string) => ({ authorization: `Bearer ${token}` }); export async function createUser(app: FastifyInstance, username: string): Promise { const res = await app.inject({ method: 'POST', url: '/auth/register', payload: { username, email: `${username}@example.com`, password: 'password123' }, }); if (res.statusCode !== 200) throw new Error(`register ${username} failed: ${res.body}`); return res.json(); } export async function follow(app: FastifyInstance, token: string, username: string) { const res = await app.inject({ method: 'POST', url: `/users/${username}/follow`, headers: bearer(token), payload: {} }); if (res.statusCode !== 200) throw new Error(`follow ${username} failed: ${res.body}`); } type FetchRoute = [match: RegExp, respond: (url: string, init?: RequestInit) => Response | Promise]; /** Replace global fetch with a router; unmatched requests fail loudly so tests never hit the network. */ export function routeFetch(routes: FetchRoute[]) { return async (input: string | URL | Request, init?: RequestInit) => { const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url; const route = routes.find(([re]) => re.test(url)); if (!route) throw new Error(`Unexpected fetch in test: ${url}`); return route[1](url, init); }; } export const json = (body: unknown, status = 200) => new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });