import { test, expect, beforeAll, afterAll, describe } from "bun:test"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { startServer, type AlignServer } from "../../src/align/server.ts"; import { addSong } from "../../src/ref/manifest.ts"; import { writeSmf } from "../../src/align/smf.ts"; import { buildSong, loadSongDir } from "../../src/bench/dataset.ts"; import type { Anchor } from "../../src/align/warp.ts"; const BIG = 120_000; let root = "", srv: AlignServer, cookie = ""; const url = (p: string) => new URL(p, srv.origin).toString(); const api = (p: string, init: RequestInit = {}) => fetch(url(p), { ...init, headers: { cookie, ...(init.headers ?? {}) }, redirect: "manual" }); const json = (body: unknown, method = "POST"): RequestInit => ({ method, body: JSON.stringify(body), headers: { "content-type": "application/json" } }); beforeAll(async () => { root = await mkdtemp(join(tmpdir(), "stsheets-align-")); const dir = await buildSong("t1a", 1); const { song, truth } = await loadSongDir(dir); const midi = join(root, "fixture.mid"); await Bun.write(midi, writeSmf(truth.tracks.map((t, i) => ({ name: t.name, program: song.tracks[i]!.program, isDrums: t.role === "drums", notes: t.notes })), song.bpm)); await addSong({ id: "fix", title: "Fixture Song", audio: join(dir, "mix.wav"), midi, capo: {} }, root); srv = await startServer({ root, host: "127.0.0.1", port: 0, quiet: true }); }, BIG); afterAll(async () => { await srv?.stop(); if (root) await rm(root, { recursive: true, force: true }); }); describe("access token", () => { test("requests without the token get 401", async () => { for (const p of ["/", "/api/songs", "/api/song/fix", "/api/songs?key=wrong"]) expect((await fetch(url(p), { redirect: "manual" })).status).toBe(401); expect((await fetch(url("/api/songs"), { headers: { cookie: "stsheets_align=nope" } })).status).toBe(401); }); test("printed URL carries the token; ?key= sets an HttpOnly cookie and redirects to a clean URL", async () => { expect(srv.url).toContain(`?key=${srv.token}`); const r = await fetch(url(`/?key=${srv.token}`), { redirect: "manual" }); expect(r.status).toBe(302); expect(r.headers.get("location")).toBe("/"); const sc = r.headers.get("set-cookie") ?? ""; expect(sc).toContain("HttpOnly"); cookie = sc.split(";")[0]!; expect(cookie).toBe(`stsheets_align=${srv.token}`); }); }); /** Raw HTTP/1.1 GET (Bun's fetch normalises "//" in paths, which would hide the bug under test). */ async function rawGet(path: string, cookieHeader = ""): Promise<{ status: number; headers: Record }> { const text = await new Promise((resolve, reject) => { let buf = ""; Bun.connect({ hostname: "127.0.0.1", port: srv.port, socket: { open(sk) { sk.write(`GET ${path} HTTP/1.1\r\nHost: 127.0.0.1\r\n${cookieHeader ? `Cookie: ${cookieHeader}\r\n` : ""}Connection: close\r\n\r\n`); }, data(sk, d) { buf += d.toString(); const end = buf.indexOf("\r\n\r\n"); // complete once headers and Content-Length bytes have arrived const len = Number(buf.slice(0, end).match(/content-length:\s*(\d+)/i)?.[1] ?? 0); if (end >= 0 && buf.length - end - 4 >= len) { sk.end(); resolve(buf); } }, close() { resolve(buf); }, error(_, e) { reject(e); }, }, }).catch(reject); }); const [head] = text.split("\r\n\r\n"); const [statusLine, ...lines] = head!.split("\r\n"); const headers = Object.fromEntries(lines.map((l) => { const i = l.indexOf(":"); return [l.slice(0, i).toLowerCase(), l.slice(i + 1).trim()]; })); return { status: Number(statusLine!.split(" ")[1]), headers }; } describe("paths with leading double slashes stay on this server", () => { test("GET //example.invalid/ is not proxied off-host (served locally: 404)", async () => { const r = await rawGet("//example.invalid/", cookie); expect(r.status).toBe(404); expect(r.headers.location).toBeUndefined(); }); test("GET /// serves the local page, not a proxied fetch", async () => { const r = await rawGet(`//127.0.0.1:${srv.port}/`, cookie); expect(r.status).toBe(404); // collapsed to /127.0.0.1:/ on the inner server }); test("the ?key= redirect keeps a same-origin Location", async () => { const r = await rawGet(`//example.invalid/x?key=${srv.token}`); expect(r.status).toBe(302); const loc = r.headers.location!; expect(loc.startsWith("/")).toBe(true); expect(loc.startsWith("//")).toBe(false); expect(new URL(loc, srv.origin).origin).toBe(new URL(srv.origin).origin); }); }); describe("page", () => { test("GET / serves the HTML page and its bundled script", async () => { const r = await api("/"); expect(r.status).toBe(200); const html = await r.text(); expect(html).toContain("]+src="([^"]+)"/)?.[1]; expect(src).toBeTruthy(); const js = await api(src!); expect(js.status).toBe(200); expect((await js.text()).length).toBeGreaterThan(1000); expect((await fetch(url(src!))).status).toBe(401); // assets are behind the token too }); }); describe("API", () => { test("songs list", async () => { expect(await (await api("/api/songs")).json()).toEqual([{ id: "fix", title: "Fixture Song", hasAlignment: false }]); }); test("unknown and path-traversal ids are 404", async () => { for (const p of ["/api/song/nope", "/api/song/%2E%2E", "/api/song/..%2F..%2Fetc", "/api/song/%2E%2E/audio", "/api/song/nope/render"]) { expect((await api(p, p.endsWith("render") ? json({ anchors: [] }) : {})).status).toBe(404); } }); let first: any; test("song: peaks, auto anchors (unsaved), stretches with MIDI and effective bpm, fit, tempo map", async () => { const r = await api("/api/song/fix"); expect(r.status).toBe(200); first = await r.json(); expect(first.id).toBe("fix"); expect(first.title).toBe("Fixture Song"); expect(first.audioDur).toBeGreaterThan(20); expect(first.midiDur).toBeGreaterThan(20); expect(first.tracks.length).toBeGreaterThan(0); expect(first.tracks.every((t: any) => typeof t.name === "string" && "role" in t)).toBe(true); expect(first.recordingPeaks.bucketSec).toBeGreaterThan(0); expect(first.recordingPeaks.max.length).toBeGreaterThan(first.audioDur * 20); expect(Math.max(...first.recordingPeaks.max)).toBeGreaterThan(0.05); expect(first.source).toBe("auto"); expect(first.savedAt).toBeNull(); expect(first.anchors.length).toBeGreaterThan(2); expect(first.anchors.every((a: Anchor) => a.kind === "auto")).toBe(true); expect(first.stretches.length).toBeGreaterThanOrEqual(first.anchors.length); // +1, minus an empty edge stretch expect(first.stretches[1].midiBpm).toBeGreaterThan(30); expect(first.stretches[1].bpm).toBeCloseTo(first.stretches[1].midiBpm / first.stretches[1].ratio, 6); expect(first.fit.matchRate).toBeGreaterThan(0.5); expect(first.midiTempo[0].bpm).toBeGreaterThan(30); expect(await Bun.file(join(root, "fix", "recording.wav")).exists()).toBe(true); expect(await Bun.file(join(root, "fix", "peaks.json")).exists()).toBe(true); }, BIG); test("audio: cached Ogg Opus", async () => { const r = await api("/api/song/fix/audio"); expect(r.status).toBe(200); expect(r.headers.get("content-type")).toContain("audio/ogg"); const b = new Uint8Array(await r.arrayBuffer()); expect(String.fromCharCode(...b.subarray(0, 4))).toBe("OggS"); expect(await Bun.file(join(root, "fix", "audio.ogg")).exists()).toBe(true); }, BIG); test("render: RIFF WAV, mono 22.05 kHz, cached by anchors", async () => { const body = json({ anchors: first.anchors }); const r = await api("/api/song/fix/render", body); expect(r.status).toBe(200); expect(r.headers.get("content-type")).toBe("audio/wav"); const b = new Uint8Array(await r.arrayBuffer()); const dv = new DataView(b.buffer); expect(String.fromCharCode(...b.subarray(0, 4))).toBe("RIFF"); expect(dv.getUint16(22, true)).toBe(1); expect(dv.getUint32(24, true)).toBe(22050); expect(b.length).toBeGreaterThan(first.audioDur * 22050 * 2); const again = await api("/api/song/fix/render", body); expect(again.headers.get("x-cache")).toBe("hit"); expect(again.headers.get("x-anchors-hash")).toBe(r.headers.get("x-anchors-hash")); }, BIG); test("fit: re-scores arbitrary anchors", async () => { const r = await api("/api/song/fix/fit", json({ anchors: [{ midiSec: 1, audioSec: 1.4, kind: "manual" }] })); const j = await r.json(); expect(j.fit.windows.length).toBeGreaterThan(0); expect(j.fit.matchRate).toBeLessThan(first.fit.matchRate); expect(j.stretches.length).toBe(2); }); test("bad anchors are rejected with 400", async () => { const bad = [{ anchors: "x" }, { anchors: [{ midiSec: 1, audioSec: 2, kind: "manual" }, { midiSec: 2, audioSec: 1, kind: "manual" }] }, { anchors: [{ midiSec: "1", audioSec: 2, kind: "manual" }] }, { anchors: [{ midiSec: 1, audioSec: 2, kind: "weird" }] }]; for (const b of bad) expect((await api("/api/song/fix/fit", json(b))).status).toBe(400); expect((await api("/api/song/fix/fit", { method: "POST", body: "{not json" })).status).toBe(400); }); test("auto: keeps manual anchors and re-solves autos around them", async () => { const manual: Anchor = { ...first.anchors[3], kind: "manual" }; const r = await api("/api/song/fix/auto", json({ anchors: [manual, ...first.anchors.slice(5)] })); const j = await r.json(); expect(j.anchors.filter((a: Anchor) => a.kind === "manual")).toEqual([manual]); expect(j.anchors.filter((a: Anchor) => a.kind === "auto").length).toBeGreaterThan(2); expect(j.fit.matchRate).toBeGreaterThan(0.5); expect(j.stretches.length).toBeGreaterThanOrEqual(j.anchors.length); }, BIG); test("reset: raw MIDI (no anchors)", async () => { const j = await (await api("/api/song/fix/reset", { method: "POST" })).json(); expect(j.anchors).toEqual([]); expect(j.stretches).toHaveLength(1); expect(j.fit.windows.length).toBeGreaterThan(0); }); test("PUT alignment then GET round-trips anchors; file holds anchors, savedAt, fit", async () => { const anchors: Anchor[] = [{ midiSec: 0.5, audioSec: 0.52, kind: "manual" }, { midiSec: 10, audioSec: 10.05, kind: "auto" }]; const p = await api("/api/song/fix/alignment", json({ anchors }, "PUT")); expect(p.status).toBe(200); const saved = await p.json(); expect(typeof saved.savedAt).toBe("string"); const file = await Bun.file(join(root, "fix", "alignment.json")).json(); expect(file.anchors).toEqual(anchors); expect(file.savedAt).toBe(saved.savedAt); expect(file.fit.windows.length).toBeGreaterThan(0); const g = await (await api("/api/song/fix")).json(); expect(g.anchors).toEqual(anchors); expect(g.source).toBe("saved"); expect(g.savedAt).toBe(saved.savedAt); expect(await (await api("/api/songs")).json()).toEqual([{ id: "fix", title: "Fixture Song", hasAlignment: true }]); }); test("more than 10k anchors is rejected with 400", async () => { const many = Array.from({ length: 10_001 }, (_, k) => ({ midiSec: k * 0.01, audioSec: k * 0.01, kind: "auto" })); expect((await api("/api/song/fix/fit", json({ anchors: many }))).status).toBe(400); }); test("a corrupt alignment.json does not break the song list; the song falls back to auto with a warning", async () => { await Bun.write(join(root, "fix", "alignment.json"), "{ not json"); const list = await api("/api/songs"); expect(list.status).toBe(200); expect(await list.json()).toEqual([{ id: "fix", title: "Fixture Song", hasAlignment: false, alignmentError: expect.stringContaining("alignment.json") }]); const g = await api("/api/song/fix"); expect(g.status).toBe(200); const j = await g.json(); expect(j.source).toBe("auto"); expect(j.warning).toContain("alignment.json"); expect(j.anchors.length).toBeGreaterThan(2); }); });