Files
gerbeur/api/test/routes/follows.test.ts

196 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/dumps?follow=users", {
auth: me.auth,
});
assertEquals(titles(feed), ["new", "old"]);
assertEquals(feed.data.items[0].categoryIds, [music]);
assertEquals(
titles(
await get("/api/dumps?follow=users&category=music", { auth: me.auth }),
),
["new"],
);
// As on the whole feed, an unknown category is a 404.
assertEquals(
(await get("/api/dumps?follow=users&category=nope", { auth: me.auth }))
.status,
404,
);
assertEquals(
(await get("/api/dumps?follow=dumps", { auth: me.auth })).status,
400,
);
assertEquals((await get("/api/dumps?follow=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/dumps?follow=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/dumps?follow=playlists", {
auth: me.auth,
})).data
.items.length,
0,
);
});