import { type ApiResponse, count, createUser, db, del, get, insertCategory, insertDump, insertPlaylist, post, 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>): 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 post(`/api/follows/users/${them.id}`, { auth: me.auth })).status, 204, ); assertEquals( (await post(`/api/follows/users/${them.id}`, { auth: me.auth })).status, 204, ); assertEquals( (await post(`/api/follows/users/${me.id}`, { auth: me.auth })).status, 400, ); assertEquals( (await get("/api/follows/status", { auth: me.auth })).data, { followedUserIds: [them.id], followedPlaylistIds: [] }, ); // Unfollow and follow again: still just the one notification. await del(`/api/follows/users/${them.id}`, { auth: me.auth }); assertEquals( (await get("/api/follows/status", { auth: me.auth })).data .followedUserIds, [], ); await post(`/api/follows/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>( `/api/users/${me.username}/followed-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 post(`/api/follows/playlists/${pub.id}`, { auth: me.auth })).status, 204, ); assertEquals( (await post(`/api/follows/playlists/${priv.id}`, { auth: me.auth })).status, 403, ); assertEquals( (await post(`/api/follows/playlists/${crypto.randomUUID()}`, { auth: me.auth, })).status, 404, ); await post(`/api/follows/playlists/${pub.id}`, { auth: owner.auth }); assertEquals( count( `SELECT COUNT(*) as n FROM notifications WHERE type = 'playlist_followed';`, ), 1, ); assertEquals( (await get>( `/api/users/${me.username}/followed-playlists`, )).data .items[0].id, pub.id, ); await del(`/api/follows/playlists/${pub.id}`, { auth: me.auth }); assertEquals( (await get("/api/follows/status", { 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 post(`/api/follows/users/${followed.id}`, { auth: me.auth }); const feed = await get>("/api/follows/feed/users", { auth: me.auth, }); assertEquals(titles(feed), ["new", "old"]); assertEquals(feed.data.items[0].categoryIds, [music]); assertEquals( titles( await get("/api/follows/feed/users?category=music", { auth: me.auth }), ), ["new"], ); // An unknown category filters everything out rather than nothing. assertEquals( (await get>("/api/follows/feed/users?category=nope", { auth: me.auth, })).data, { items: [], hasMore: false }, ); assertEquals((await get("/api/follows/feed/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 post(`/api/follows/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>("/api/follows/feed/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>("/api/follows/feed/playlists", { auth: me.auth, })).data .items.length, 0, ); });