Validation: the dump validators refuse the nulls their shared types don't allow (a null title or note on creation, a null URL on edit); a playlist title of spaces is refused and titles are stored trimmed; usernames and emails are checked by the same USERNAME_RE and EMAIL_RE in shared/validation.ts on sign-up, on PATCH /api/users/me (which checked other limits) and in the sign-up form. Mentions are read by shared/mention.ts's mentionedUsernames, which skips code and URLs as Markdown shows them: https://mastodon.social/@bob or a `@bob` in code notified bob; the editor's suggestions derive their trigger from the same pattern instead of a copy. Following an unknown user is a 404 rather than a foreign-key 500, and following a private playlist a 404 rather than a 403 that confirmed it exists. `ws` and `emojibase` are reserved category slugs, since the server answers them before the app. GET /api/users/me no longer turns every error into a 500. Dead code goes: adminOnlyMiddleware, verifyEmailTransport, listUsers, deleteUser with disconnectUser and the force_logout message only it sent, PATCH /api/notifications/:id/read (with markOneRead), isSlugAvailable, the unreachable 500 in fetchComment, and the `if (!ctx.state.user)` checks behind authMiddleware. 214 backend and 627 frontend tests pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
69 lines
2.0 KiB
TypeScript
69 lines
2.0 KiB
TypeScript
import {
|
|
count,
|
|
createUser,
|
|
db,
|
|
insertDump,
|
|
patch,
|
|
post,
|
|
test,
|
|
} from "../helpers.ts";
|
|
import type { Comment } from "../../../shared/comment.ts";
|
|
import { assertEquals } from "@std/assert";
|
|
|
|
test("mentions: notify the named, once per context, never the author or an email domain", async () => {
|
|
const author = await createUser();
|
|
const named = await createUser();
|
|
const { id: dumpId } = insertDump({ userId: author.id, title: "Topic" });
|
|
const body = `hi @${named.username.toUpperCase()} and @${named.username}, ` +
|
|
`me @${author.username}, mail x@${named.username}.com, ` +
|
|
`[@${named.username}](link) @nobody_known`;
|
|
const c = await post<Comment>(`/api/dumps/${dumpId}/comments`, {
|
|
auth: author.auth,
|
|
json: { body },
|
|
});
|
|
const notes = db.prepare(
|
|
`SELECT user_id, data FROM notifications WHERE type = 'user_mentioned';`,
|
|
).all() as { user_id: string; data: string }[];
|
|
assertEquals(notes.length, 1);
|
|
assertEquals(notes[0].user_id, named.id);
|
|
assertEquals(JSON.parse(notes[0].data), {
|
|
mentionerId: author.id,
|
|
mentionerUsername: author.username,
|
|
contextType: "comment",
|
|
contextId: c.data.id,
|
|
contextTitle: "Topic",
|
|
dumpId,
|
|
});
|
|
// Editing the comment doesn't notify a second time.
|
|
await patch(`/api/comments/${c.data.id}`, {
|
|
auth: author.auth,
|
|
json: { body: `again @${named.username}` },
|
|
});
|
|
assertEquals(
|
|
count(
|
|
`SELECT COUNT(*) as n FROM notifications WHERE type = 'user_mentioned';`,
|
|
),
|
|
1,
|
|
);
|
|
});
|
|
|
|
test("mentions: code and URLs mention no one", async () => {
|
|
const author = await createUser();
|
|
const named = await createUser();
|
|
const { id: dumpId } = insertDump({ userId: author.id });
|
|
const n = named.username;
|
|
await post(`/api/dumps/${dumpId}/comments`, {
|
|
auth: author.auth,
|
|
json: {
|
|
body:
|
|
`\`@${n}\` and https://mastodon.social/@${n}\n\`\`\`\n@${n}\n\`\`\``,
|
|
},
|
|
});
|
|
assertEquals(
|
|
count(
|
|
`SELECT COUNT(*) as n FROM notifications WHERE type = 'user_mentioned';`,
|
|
),
|
|
0,
|
|
);
|
|
});
|