import { describe, expect, it } from 'vitest'; import { db, schema } from '../src/db'; import { applyPollResult } from '../src/services/nowPlaying'; import { bearer, createUser, follow, useTestApp } from './helpers'; const app = useTestApp(); const get = (url: string, token?: string) => app().inject({ url, headers: token ? bearer(token) : {} }); const post = (url: string, token: string, payload: object = {}) => app().inject({ method: 'POST', url, headers: bearer(token), payload }); const del = (url: string, token: string) => app().inject({ method: 'DELETE', url, headers: bearer(token) }); const names = (list: { users: { username: string }[] }) => list.users.map((u) => u.username); describe('followers and following lists', () => { it('lists followers and following, newest first, with the viewer’s follow state', async () => { const alice = await createUser(app(), 'alice'); const bob = await createUser(app(), 'bob'); const carol = await createUser(app(), 'carol'); await follow(app(), bob.token, 'alice'); await follow(app(), carol.token, 'alice'); await follow(app(), alice.token, 'carol'); const followers = (await get('/users/alice/followers', alice.token)).json(); expect(names(followers)).toEqual(['carol', 'bob']); expect(followers.users.find((u: { username: string }) => u.username === 'carol').isFollowing).toBe(true); expect(followers.nextCursor).toBeNull(); expect(names((await get('/users/alice/following')).json())).toEqual(['carol']); }); it('paginates', { timeout: 60_000 }, async () => { const alice = await createUser(app(), 'alice'); for (let i = 0; i < 55; i++) { const f = await createUser(app(), `fan${i}`); await follow(app(), f.token, 'alice'); } const first = (await get('/users/alice/followers', alice.token)).json(); expect(first.users).toHaveLength(50); const second = (await get(`/users/alice/followers?cursor=${first.nextCursor}`, alice.token)).json(); expect(second.users).toHaveLength(5); expect(second.nextCursor).toBeNull(); }); it('shows "follows you" on profiles', async () => { const alice = await createUser(app(), 'alice'); const bob = await createUser(app(), 'bob'); await follow(app(), bob.token, 'alice'); expect((await get('/users/bob', alice.token)).json().followsYou).toBe(true); expect((await get('/users/alice', bob.token)).json().followsYou).toBe(false); }); it('removing a follower takes them off your list', async () => { const alice = await createUser(app(), 'alice'); const bob = await createUser(app(), 'bob'); await follow(app(), bob.token, 'alice'); expect((await del('/me/followers/bob', alice.token)).statusCode).toBe(200); expect(names((await get('/users/alice/followers', alice.token)).json())).toEqual([]); expect((await get('/feed', bob.token)).json()).toEqual([]); }); }); describe('blocking', () => { async function blockedPair() { const alice = await createUser(app(), 'alice'); const bob = await createUser(app(), 'bob'); await follow(app(), alice.token, 'bob'); await follow(app(), bob.token, 'alice'); await applyPollResult(alice.user.id, 'spotify', { track: { title: 'Secret', artist: 'X' }, isPlaying: true, progressMs: 0 }); expect((await post('/users/bob/block', alice.token)).statusCode).toBe(200); return { alice, bob }; } it('severs follows in both directions', async () => { const { alice, bob } = await blockedPair(); expect(await db.select().from(schema.follows)).toHaveLength(0); expect((await get('/feed', bob.token)).json()).toEqual([]); expect((await get('/feed', alice.token)).json()).toEqual([]); }); it('hides the blocker from the blocked person entirely', async () => { const { bob } = await blockedPair(); expect((await get('/users/alice', bob.token)).statusCode).toBe(404); expect((await get('/users/alice/followers', bob.token)).statusCode).toBe(404); expect((await get('/users/search?q=ali', bob.token)).json()).toEqual([]); expect((await post('/users/alice/follow', bob.token)).statusCode).toBe(404); }); it('still shows them to everyone else', async () => { await blockedPair(); const carol = await createUser(app(), 'carol'); expect((await get('/users/alice', carol.token)).json().nowPlaying.track.title).toBe('Secret'); }); it("the blocker sees a profile with activity hidden, and can unblock", async () => { const { alice, bob } = await blockedPair(); await applyPollResult(bob.user.id, 'spotify', { track: { title: 'Bobs song', artist: 'Y' }, isPlaying: true, progressMs: 0 }); const profile = (await get('/users/bob', alice.token)).json(); expect(profile).toMatchObject({ isBlocked: true, nowPlaying: null, recent: [], featured: null }); expect((await post('/users/bob/follow', alice.token)).statusCode).toBe(400); expect((await get('/me/blocks', alice.token)).json().map((u: { username: string }) => u.username)).toEqual(['bob']); await del('/users/bob/block', alice.token); expect((await get('/users/alice', bob.token)).statusCode).toBe(200); expect((await post('/users/bob/follow', alice.token)).statusCode).toBe(200); }); it("can't block yourself", async () => { const alice = await createUser(app(), 'alice'); expect((await post('/users/alice/block', alice.token)).statusCode).toBe(400); }); }); describe('reporting', () => { it('stores a report for moderation', async () => { const alice = await createUser(app(), 'alice'); await createUser(app(), 'spammer'); const res = await post('/users/spammer/report', alice.token, { reason: 'spam', note: 'posting links' }); expect(res.statusCode).toBe(200); const [report] = await db.select().from(schema.reports); expect(report).toMatchObject({ reason: 'spam', note: 'posting links', status: 'open', reporterId: alice.user.id }); }); it('validates the reason and refuses self-reports', async () => { const alice = await createUser(app(), 'alice'); await createUser(app(), 'bob'); expect((await post('/users/bob/report', alice.token, { reason: 'boring' })).statusCode).toBe(400); expect((await post('/users/alice/report', alice.token, { reason: 'spam' })).statusCode).toBe(400); }); });