169 lines
5.8 KiB
TypeScript
169 lines
5.8 KiB
TypeScript
import "../../setup.ts";
|
|
import { assertEquals } from "@std/assert";
|
|
import { DatabaseSync } from "node:sqlite";
|
|
import { up as up0021 } from "../../../db/migrations/0021_page_urls.ts";
|
|
|
|
/** A database as v3 left it: schema.sql without 0021's tables and indexes. */
|
|
function v3Db(): DatabaseSync {
|
|
const db = new DatabaseSync(":memory:");
|
|
db.exec(
|
|
Deno.readTextFileSync(new URL("../../../db/schema.sql", import.meta.url)),
|
|
);
|
|
db.exec(`
|
|
DROP TABLE dump_slug_aliases;
|
|
DROP TABLE playlist_slug_aliases;
|
|
DROP TABLE user_aliases;
|
|
DROP INDEX idx_dumps_user_slug;
|
|
DROP INDEX idx_playlists_user_slug;
|
|
CREATE INDEX idx_dumps_slug ON dumps(slug);
|
|
CREATE INDEX idx_playlists_slug ON playlists(slug);
|
|
`);
|
|
return db;
|
|
}
|
|
|
|
const NOW = "2024-01-01T00:00:00.000Z";
|
|
const later = (n: number) => new Date(Date.parse(NOW) + n * 1000).toISOString();
|
|
|
|
function user(db: DatabaseSync, username: string): string {
|
|
const id = crypto.randomUUID();
|
|
db.prepare(
|
|
`INSERT INTO users (id, username, password_hash, created_at, email) VALUES (?, ?, 'x', ?, ?);`,
|
|
).run(id, username, NOW, `${username}@example.test`);
|
|
return id;
|
|
}
|
|
|
|
/** Inserts a dump or playlist with its v3 slug, and returns its id and slug. */
|
|
function item(
|
|
db: DatabaseSync,
|
|
table: "dumps" | "playlists",
|
|
userId: string,
|
|
title: string,
|
|
at: number,
|
|
): { id: string; v3: string } {
|
|
const id = crypto.randomUUID();
|
|
const v3 = `${title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${
|
|
id.slice(0, 8)
|
|
}`;
|
|
db.prepare(
|
|
table === "dumps"
|
|
? `INSERT INTO dumps (id, kind, title, slug, user_id, created_at, url) VALUES (?, 'url', ?, ?, ?, ?, 'https://x.test');`
|
|
: `INSERT INTO playlists (id, title, slug, user_id, created_at) VALUES (?, ?, ?, ?, ?);`,
|
|
).run(id, title, v3, userId, later(at));
|
|
return { id, v3 };
|
|
}
|
|
|
|
const slugOf = (db: DatabaseSync, table: string, id: string) =>
|
|
(db.prepare(`SELECT slug FROM ${table} WHERE id = ?;`).get(id) as {
|
|
slug: string;
|
|
}).slug;
|
|
|
|
Deno.test("migration 0021: bare slugs unique per owner, reserved categories renamed", () => {
|
|
const db = v3Db();
|
|
const bob = user(db, "Bob");
|
|
const ann = user(db, "ann");
|
|
const first = item(db, "dumps", bob, "Never Gonna", 0);
|
|
const second = item(db, "dumps", bob, "Never gonna!", 1);
|
|
const anns = item(db, "dumps", ann, "Never Gonna", 2);
|
|
const mix = item(db, "playlists", bob, "Never Gonna", 3);
|
|
for (
|
|
const [slug, name] of [
|
|
["hot", "Hot"],
|
|
["hot-2", "Hot 2"],
|
|
["follow", "F"],
|
|
["music", "M"],
|
|
]
|
|
) {
|
|
db.prepare(
|
|
`INSERT INTO categories (id, slug, name, created_at) VALUES (?, ?, ?, ?);`,
|
|
).run(crypto.randomUUID(), slug, name, NOW);
|
|
}
|
|
|
|
up0021(db);
|
|
|
|
// The oldest keeps the bare slug; each kind and owner is its own space.
|
|
assertEquals(slugOf(db, "dumps", first.id), "never-gonna");
|
|
assertEquals(slugOf(db, "dumps", second.id), "never-gonna-2");
|
|
assertEquals(slugOf(db, "dumps", anns.id), "never-gonna");
|
|
assertEquals(slugOf(db, "playlists", mix.id), "never-gonna");
|
|
assertEquals(
|
|
(db.prepare(`SELECT slug FROM categories ORDER BY slug;`).all() as {
|
|
slug: string;
|
|
}[]).map((r) => r.slug),
|
|
["follow-2", "hot-2", "hot-3", "music"],
|
|
);
|
|
// The new index holds.
|
|
let clash = false;
|
|
try {
|
|
db.prepare(`UPDATE dumps SET slug = 'never-gonna' WHERE id = ?;`).run(
|
|
second.id,
|
|
);
|
|
} catch {
|
|
clash = true;
|
|
}
|
|
assertEquals(clash, true);
|
|
db.close();
|
|
});
|
|
|
|
Deno.test("migration 0021: v3 links in notes, comments and chat become v4 paths", () => {
|
|
const db = v3Db();
|
|
const bob = user(db, "Bob");
|
|
const dump = item(db, "dumps", bob, "Never Gonna", 0);
|
|
const mix = item(db, "playlists", bob, "Sunday Mix", 1);
|
|
const text = [
|
|
`[by slug](http://gerbeur.test/dumps/${dump.v3}#comment-1)`,
|
|
`[relative by id](/dumps/${dump.id}/edit)`,
|
|
`http://gerbeur.test/playlists/${mix.id}?x=1`,
|
|
`<http://gerbeur.test/users/bob/~/followed>`,
|
|
`[list](/users/BOB/upvoted) [settings](/users/bob/~/settings)`,
|
|
`[feed](/~/followed) [search](/search/~/users?q=a)`,
|
|
// Another site's, a bare path in prose and a deleted dump stay as they are.
|
|
`https://elsewhere.test/dumps/${dump.v3} see /dumps/${dump.v3}`,
|
|
`http://gerbeur.test/dumps/gone-12345678`,
|
|
].join("\n");
|
|
const expected = [
|
|
`[by slug](http://gerbeur.test/@Bob/dumps/never-gonna#comment-1)`,
|
|
`[relative by id](/@Bob/dumps/never-gonna/edit)`,
|
|
`http://gerbeur.test/@Bob/playlists/sunday-mix?x=1`,
|
|
`<http://gerbeur.test/@Bob/follow>`,
|
|
`[list](/@Bob/upvoted) [settings](/settings)`,
|
|
`[feed](/follow) [search](/search/users?q=a)`,
|
|
`https://elsewhere.test/dumps/${dump.v3} see /dumps/${dump.v3}`,
|
|
`http://gerbeur.test/dumps/gone-12345678`,
|
|
].join("\n");
|
|
const commentId = crypto.randomUUID();
|
|
const messageId = crypto.randomUUID();
|
|
db.prepare(`UPDATE dumps SET comment = ? WHERE id = ?;`).run(text, dump.id);
|
|
db.prepare(`UPDATE playlists SET description = ? WHERE id = ?;`).run(
|
|
text,
|
|
mix.id,
|
|
);
|
|
db.prepare(
|
|
`INSERT INTO comments (id, dump_id, user_id, body, created_at) VALUES (?, ?, ?, ?, ?);`,
|
|
).run(commentId, dump.id, bob, text, NOW);
|
|
db.prepare(
|
|
`INSERT INTO chat_messages (id, user_id, body, created_at) VALUES (?, ?, ?, ?);`,
|
|
).run(messageId, bob, text, NOW);
|
|
|
|
up0021(db);
|
|
|
|
const read = (sql: string, id: string) =>
|
|
(db.prepare(sql).get(id) as { text: string }).text;
|
|
assertEquals(
|
|
read(`SELECT comment AS text FROM dumps WHERE id = ?;`, dump.id),
|
|
expected,
|
|
);
|
|
assertEquals(
|
|
read(`SELECT body AS text FROM comments WHERE id = ?;`, commentId),
|
|
expected,
|
|
);
|
|
assertEquals(
|
|
read(`SELECT body AS text FROM chat_messages WHERE id = ?;`, messageId),
|
|
expected,
|
|
);
|
|
assertEquals(
|
|
read(`SELECT description AS text FROM playlists WHERE id = ?;`, mix.id),
|
|
expected,
|
|
);
|
|
db.close();
|
|
});
|