193 lines
5.8 KiB
TypeScript
193 lines
5.8 KiB
TypeScript
import {
|
|
type ApiResponse,
|
|
count,
|
|
createUser,
|
|
db,
|
|
del,
|
|
get,
|
|
insertCategory,
|
|
insertDump,
|
|
insertPlaylist,
|
|
post,
|
|
put,
|
|
test,
|
|
} from "../helpers.ts";
|
|
import type { Dump } from "../../../shared/dump.ts";
|
|
import type { FollowStatus } from "../../../shared/follow.ts";
|
|
import type { PublicUser } from "../../../shared/user.ts";
|
|
import type { PaginatedData } from "../../../shared/api.ts";
|
|
import type { Playlist } from "../../../shared/playlist.ts";
|
|
import { assertEquals, assertFalse } from "@std/assert";
|
|
|
|
function titles(res: ApiResponse<PaginatedData<Dump>>): string[] {
|
|
return res.data.items.map((d) => d.title);
|
|
}
|
|
|
|
test("following users: status, idempotence, no self-follow, one notification", async () => {
|
|
const me = await createUser();
|
|
const them = await createUser();
|
|
assertEquals(
|
|
(await put(`/api/me/follow/users/${them.id}`, { auth: me.auth })).status,
|
|
204,
|
|
);
|
|
assertEquals(
|
|
(await put(`/api/me/follow/users/${them.id}`, { auth: me.auth })).status,
|
|
204,
|
|
);
|
|
assertEquals(
|
|
(await put(`/api/me/follow/users/${me.id}`, { auth: me.auth })).status,
|
|
400,
|
|
);
|
|
assertEquals(
|
|
(await put(`/api/me/follow/users/${crypto.randomUUID()}`, {
|
|
auth: me.auth,
|
|
})).status,
|
|
404,
|
|
);
|
|
assertEquals(
|
|
(await get<FollowStatus>("/api/me/follow", { auth: me.auth })).data,
|
|
{ followedUserIds: [them.id], followedPlaylistIds: [] },
|
|
);
|
|
// Unfollow and follow again: still just the one notification.
|
|
await del(`/api/me/follow/users/${them.id}`, { auth: me.auth });
|
|
assertEquals(
|
|
(await get<FollowStatus>("/api/me/follow", { auth: me.auth })).data
|
|
.followedUserIds,
|
|
[],
|
|
);
|
|
await put(`/api/me/follow/users/${them.id}`, { auth: me.auth });
|
|
assertEquals(
|
|
count(
|
|
`SELECT COUNT(*) as n FROM notifications WHERE user_id = ? AND type = 'user_followed';`,
|
|
them.id,
|
|
),
|
|
1,
|
|
);
|
|
const list = await get<PaginatedData<PublicUser>>(
|
|
`/api/users/@${me.username}/follow/users`,
|
|
);
|
|
assertEquals(list.data.items.map((u) => u.id), [
|
|
them.id,
|
|
]);
|
|
assertFalse("email" in list.data.items[0]);
|
|
});
|
|
|
|
test("following playlists: public ones only, and not your own notification", async () => {
|
|
const owner = await createUser();
|
|
const me = await createUser();
|
|
const pub = insertPlaylist({ userId: owner.id });
|
|
const priv = insertPlaylist({ userId: owner.id, isPublic: false });
|
|
assertEquals(
|
|
(await put(`/api/me/follow/playlists/${pub.id}`, { auth: me.auth })).status,
|
|
204,
|
|
);
|
|
assertEquals(
|
|
(await put(`/api/me/follow/playlists/${priv.id}`, { auth: me.auth }))
|
|
.status,
|
|
404,
|
|
);
|
|
assertEquals(
|
|
(await put(`/api/me/follow/playlists/${crypto.randomUUID()}`, {
|
|
auth: me.auth,
|
|
})).status,
|
|
404,
|
|
);
|
|
await put(`/api/me/follow/playlists/${pub.id}`, { auth: owner.auth });
|
|
assertEquals(
|
|
count(
|
|
`SELECT COUNT(*) as n FROM notifications WHERE type = 'playlist_followed';`,
|
|
),
|
|
1,
|
|
);
|
|
assertEquals(
|
|
(await get<PaginatedData<Playlist>>(
|
|
`/api/users/@${me.username}/follow/playlists`,
|
|
)).data
|
|
.items[0].id,
|
|
pub.id,
|
|
);
|
|
await del(`/api/me/follow/playlists/${pub.id}`, { auth: me.auth });
|
|
assertEquals(
|
|
(await get<FollowStatus>("/api/me/follow", { auth: me.auth })).data
|
|
.followedPlaylistIds,
|
|
[],
|
|
);
|
|
});
|
|
|
|
test("followed-users feed: public dumps of the people I follow, newest first", async () => {
|
|
const me = await createUser();
|
|
const followed = await createUser();
|
|
const stranger = await createUser();
|
|
const music = insertCategory({ slug: "music" });
|
|
insertDump({
|
|
userId: followed.id,
|
|
title: "old",
|
|
createdAt: "2024-01-01T00:00:00Z",
|
|
});
|
|
const tagged = insertDump({
|
|
userId: followed.id,
|
|
title: "new",
|
|
createdAt: "2024-02-01T00:00:00Z",
|
|
});
|
|
db.prepare(
|
|
`INSERT INTO dump_categories (dump_id, category_id) VALUES (?, ?);`,
|
|
)
|
|
.run(tagged.id, music);
|
|
insertDump({ userId: followed.id, title: "private", isPrivate: true });
|
|
insertDump({ userId: stranger.id, title: "stranger" });
|
|
await put(`/api/me/follow/users/${followed.id}`, { auth: me.auth });
|
|
|
|
const feed = await get<PaginatedData<Dump>>("/api/me/feed?from=users", {
|
|
auth: me.auth,
|
|
});
|
|
assertEquals(titles(feed), ["new", "old"]);
|
|
assertEquals(feed.data.items[0].categoryIds, [music]);
|
|
assertEquals(
|
|
titles(
|
|
await get("/api/me/feed?from=users&category=music", { auth: me.auth }),
|
|
),
|
|
["new"],
|
|
);
|
|
// An unknown category filters everything out rather than nothing.
|
|
assertEquals(
|
|
(await get<PaginatedData<Dump>>("/api/me/feed?from=users&category=nope", {
|
|
auth: me.auth,
|
|
})).data,
|
|
{ items: [], hasMore: false },
|
|
);
|
|
assertEquals((await get("/api/me/feed?from=users")).status, 401);
|
|
});
|
|
|
|
test("followed-playlists feed: each dump once, public playlists only", async () => {
|
|
const me = await createUser();
|
|
const owner = await createUser();
|
|
const p1 = insertPlaylist({ userId: owner.id });
|
|
const p2 = insertPlaylist({ userId: owner.id });
|
|
const d = insertDump({ userId: owner.id, title: "shared" });
|
|
const priv = insertDump({
|
|
userId: owner.id,
|
|
title: "private",
|
|
isPrivate: true,
|
|
});
|
|
for (const p of [p1, p2]) {
|
|
await put(`/api/me/follow/playlists/${p.id}`, { auth: me.auth });
|
|
await post(`/api/playlists/${p.id}/dumps/${d.id}`, { auth: owner.auth });
|
|
}
|
|
await post(`/api/playlists/${p1.id}/dumps/${priv.id}`, { auth: owner.auth });
|
|
const feed = await get<PaginatedData<Dump>>("/api/me/feed?from=playlists", {
|
|
auth: me.auth,
|
|
});
|
|
assertEquals(titles(feed), ["shared"]);
|
|
assertEquals(feed.data.items.length, 1);
|
|
|
|
// A playlist gone private drops out of the feed.
|
|
db.prepare(`UPDATE playlists SET is_public = 0;`).run();
|
|
assertEquals(
|
|
(await get<PaginatedData<Dump>>("/api/me/feed?from=playlists", {
|
|
auth: me.auth,
|
|
})).data
|
|
.items.length,
|
|
0,
|
|
);
|
|
});
|