Files
musicshare/apps/api/test/helpers.ts
T
chrisandClaude Opus 5.5 fe503307df Add unit and integration test suites
- Shared: link building, feed ordering, input schemas, API client
- API unit: track matching, private-address blocking, token crypto,
  plan expiry, provider response parsing
- API integration against Postgres and Redis: auth, follows and pins,
  now-playing state, webhooks, OAuth return URLs, realtime, rate limits
- Tests use a dedicated *_test database and Redis DB, and refuse to wipe
  anything else; fetch is stubbed so they never hit the network
- Pin Node to 22.13+ (.nvmrc), which Vite's native bindings require

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
2026-10-03 11:28:17 -04:00

73 lines
3.0 KiB
TypeScript

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 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<AuthResponse> {
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<Response>];
/** 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' } });