fix(align): review round 1 for the alignment page
- server: collapse leading slashes so //host/x is never proxied off-host or redirected cross-origin; 1 MB body limit; max 10k anchors; a corrupt alignment.json no longer 500s /api/songs (song falls back to auto with a warning); comments on cookie scope and the loopback inner server - warp (ruling A6): adding/moving a manual anchor removes autos only within 10 s (MIDI) of the new/old position, never past a manual, plus any auto that would break audio monotonicity - page: first anchor clamped at >= 0 s; ruler scrubbing seeks on release; wording for the new auto-removal rule; removed-count excludes the dragged anchor; shows server warnings - manifest: unique tmp name for alignment.json; clear error for a corrupt file - resample test: speed asserted as a ratio to the direct path Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
894fd9b9ef
commit
f0249995cf
@@ -42,6 +42,51 @@ describe("access 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<string, string> }> {
|
||||
const text = await new Promise<string>((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 //<this server>/ 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:<port>/ 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("/");
|
||||
@@ -158,4 +203,20 @@ describe("API", () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user