Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions apps/web/lib/social.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { createClient, type Client, type InStatement } from "@libsql/client";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { getMostFollowed, getMostFollowing } from "./social";

const mocks = vi.hoisted(() => ({ execute: vi.fn() }));

vi.mock("server-only", () => ({}));
vi.mock("./db", () => ({
sqlClient: { execute: mocks.execute },
}));

let client: Client;

describe("follow leaderboards", () => {
beforeAll(async () => {
client = createClient({ url: "file::memory:" });
mocks.execute.mockImplementation((statement: InStatement) => client.execute(statement));
await client.execute(`CREATE TABLE users (
id TEXT PRIMARY KEY,
display_name TEXT,
status TEXT NOT NULL
)`);
await client.execute(`CREATE TABLE follows (
follower_id TEXT NOT NULL,
followee_id TEXT NOT NULL,
PRIMARY KEY (follower_id, followee_id)
)`);
});

beforeEach(async () => {
await client.execute("DELETE FROM follows");
await client.execute("DELETE FROM users");
await client.execute(`INSERT INTO users (id, display_name, status) VALUES
('alice', 'Alice', 'active'),
('bob', 'Bob', 'active'),
('suspended', 'Suspended', 'suspended'),
('deleted', 'Deleted', 'deleted')`);
await client.execute(`INSERT INTO follows (follower_id, followee_id) VALUES
('bob', 'alice'),
('suspended', 'alice'),
('deleted', 'alice'),
('alice', 'bob'),
('alice', 'suspended'),
('alice', 'deleted')`);
});

afterAll(() => {
client.close();
});

it("excludes inactive followers from the most-followed counts", async () => {
const rows = await getMostFollowed();

expect(rows.find((row) => row.userId === "alice")?.count).toBe(1);
expect(rows.some((row) => row.userId === "suspended" || row.userId === "deleted")).toBe(false);
});

it("excludes inactive followees from the most-following counts", async () => {
const rows = await getMostFollowing();

expect(rows.find((row) => row.userId === "alice")?.count).toBe(1);
expect(rows.some((row) => row.userId === "suspended" || row.userId === "deleted")).toBe(false);
});
});
9 changes: 6 additions & 3 deletions apps/web/lib/social.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,13 @@ export type FollowRankRow = { rank: number; userId: string; displayName: string;
async function followRanking(kind: "followers" | "following", limit: number): Promise<FollowRankRow[]> {
// followers = ranked by inbound edges (followee_id); following = outbound (follower_id).
const groupCol = kind === "followers" ? "followee_id" : "follower_id";
const countedCol = kind === "followers" ? "follower_id" : "followee_id";
const r = await sqlClient.execute({
sql: `SELECT f.${groupCol} AS uid, COUNT(*) AS c, u.display_name AS name
FROM follows f JOIN users u ON u.id = f.${groupCol}
WHERE u.status = 'active'
sql: `SELECT f.${groupCol} AS uid, COUNT(*) AS c, ranked.display_name AS name
FROM follows f
JOIN users ranked ON ranked.id = f.${groupCol}
JOIN users counted ON counted.id = f.${countedCol}
WHERE ranked.status = 'active' AND counted.status = 'active'
GROUP BY f.${groupCol}
ORDER BY c DESC, name ASC
LIMIT ?`,
Expand Down
Loading