import { test, expect, beforeAll, afterAll, beforeEach, afterEach } from "bun:test"; import { setupTestDb, teardownTestDb } from "db/test-utils"; import * as dbschema from "db/schema"; import type { BlobRef } from "db/schema"; import { createRouter } from "../index"; import { faker } from "@faker-js/faker"; import { fakeDid, fakeCid, fakeTid, fakeBlobRef } from "shared/factories"; import type { XRPCRouter } from "@atcute/xrpc-server"; let db: Awaited>; let txDb: typeof db; let rollback: () => void; beforeAll(async () => { db = await setupTestDb(); }); afterAll(async () => { await teardownTestDb(); }); beforeEach(async () => { await new Promise((resolveSetup) => { let rejectTx: (err: Error) => void; db.transaction(async (tx) => { txDb = tx as unknown as typeof db; resolveSetup(); await new Promise((_, reject) => { rejectTx = reject; }); }).catch(() => {}); rollback = () => rejectTx(new Error("rollback")); }); }); afterEach(() => { rollback(); }); const COLLECTION = "ca.ansxor.catnip.track"; async function insertTrack( db: typeof txDb, overrides?: { did?: `did:${string}:${string}`; rkey?: string; cid?: string; title?: string; audio?: BlobRef; artists?: { did?: string; name?: string }[]; }, ) { const did = overrides?.did ?? fakeDid(); const rkey = overrides?.rkey ?? fakeTid(); const uri = `at://${did}/${COLLECTION}/${rkey}`; const cid = overrides?.cid ?? fakeCid(); const title = overrides?.title ?? faker.music.songName(); const audio = overrides?.audio ?? fakeBlobRef(); await db.insert(dbschema.tracks).values({ uri, did, rkey, cid, title, createdAt: new Date(), audio, }); const artists = overrides?.artists ?? [{ did, name: faker.person.fullName() }]; for (let i = 0; i < artists.length; i++) { const a = artists[i]!; const [inserted] = await db .insert(dbschema.artists) .values({ did: a.did ?? null, name: a.name ?? null }) .onConflictDoNothing() .returning(); const artistId = inserted?.id ?? (await db.query.artists.findFirst({ where: (art, { eq }) => eq(art.did, a.did!), }))!.id; await db.insert(dbschema.trackArtists).values({ trackUri: uri, artistId, position: i, }); } return { uri, did, rkey, cid, title, audio }; } function makeRouter() { return createRouter(txDb); } async function fetchTracks(router: XRPCRouter, uris: string[]) { const url = new URL("http://localhost/xrpc/ca.ansxor.catnip.getTracks"); for (const uri of uris) { url.searchParams.append("uris", uri); } return router.fetch(new Request(url, { method: "GET" })); } async function fetchTracksByDid(router: XRPCRouter, did: string) { const url = new URL("http://localhost/xrpc/ca.ansxor.catnip.getTracksByDid"); url.searchParams.set("did", did); return router.fetch(new Request(url, { method: "GET" })); } test("returns a track by URI", async () => { const title = faker.music.songName(); const artistName = faker.person.fullName(); const did = fakeDid(); const { uri } = await insertTrack(txDb, { did, title, artists: [{ did, name: artistName }], }); const router = makeRouter(); const response = await fetchTracks(router, [uri]); expect(response.status).toBe(200); const data = await response.json(); expect(data.tracks).toHaveLength(1); const track = data.tracks[0]; expect(track.title).toBe(title); expect(track.$type).toBe("ca.ansxor.catnip.track"); expect(track.artists).toHaveLength(1); expect(track.artists[0].did).toBe(did); expect(track.artists[0].name).toBe(artistName); expect(track.audio).toBeDefined(); }); test("returns empty array for unknown URIs", async () => { const router = makeRouter(); const response = await fetchTracks(router, [ `at://${fakeDid()}/${COLLECTION}/${fakeTid()}`, ]); expect(response.status).toBe(200); const data = await response.json(); expect(data.tracks).toHaveLength(0); }); test("returns multiple tracks", async () => { const { uri: uri1 } = await insertTrack(txDb, { title: "Track One" }); const { uri: uri2 } = await insertTrack(txDb, { title: "Track Two" }); const router = makeRouter(); const response = await fetchTracks(router, [uri1, uri2]); expect(response.status).toBe(200); const data = await response.json(); expect(data.tracks).toHaveLength(2); const titles = data.tracks.map((t: any) => t.title).sort(); expect(titles).toEqual(["Track One", "Track Two"]); }); // getTracksByDid tests test("getTracksByDid returns tracks for a given DID", async () => { const did = fakeDid(); await insertTrack(txDb, { did, title: "DID Track 1" }); await insertTrack(txDb, { did, title: "DID Track 2" }); const router = makeRouter(); const response = await fetchTracksByDid(router, did); expect(response.status).toBe(200); const data = await response.json(); expect(data.tracks).toHaveLength(2); const titles = data.tracks.map((t: any) => t.title).sort(); expect(titles).toEqual(["DID Track 1", "DID Track 2"]); }); test("getTracksByDid returns empty array for unknown DID", async () => { const router = makeRouter(); const response = await fetchTracksByDid(router, fakeDid()); expect(response.status).toBe(200); const data = await response.json(); expect(data.tracks).toHaveLength(0); }); test("getTracksByDid only returns tracks from the specified DID", async () => { const did1 = fakeDid(); const did2 = fakeDid(); await insertTrack(txDb, { did: did1, title: "User 1 Track" }); await insertTrack(txDb, { did: did2, title: "User 2 Track" }); const router = makeRouter(); const response = await fetchTracksByDid(router, did1); expect(response.status).toBe(200); const data = await response.json(); expect(data.tracks).toHaveLength(1); expect(data.tracks[0].title).toBe("User 1 Track"); });