import { eq } from 'drizzle-orm'; import { describe, expect, it } from 'vitest'; import { db, schema } from '../src/db'; import { redis } from '../src/lib/redis'; import { applyPollResult } from '../src/services/nowPlaying'; import { upsertConnection } from '../src/services/connections'; import { bearer, createUser, useTestApp } from './helpers'; const app = useTestApp(); const setSharing = (token: string, pause: string | null) => app().inject({ method: 'PUT', url: '/me/sharing', headers: bearer(token), payload: { pause } }); async function deviceFor(token: string) { const res = await app().inject({ method: 'POST', url: '/me/devices', headers: bearer(token), payload: { platform: 'android', name: 'Pixel' } }); return res.json().token as string; } const report = (deviceToken: string, title: string) => app().inject({ method: 'POST', url: '/devices/now-playing', headers: bearer(deviceToken), payload: { app: 'com.spotify.music', state: 'playing', track: { title, artist: 'New Order' } }, }); describe('pausing sharing', () => { it.each([ ['1h', 3600_000], ['8h', 8 * 3600_000], ])('pause %s sets a resume time', async (pause, ms) => { const { token } = await createUser(app(), 'alice'); const me = (await setSharing(token, pause)).json(); const until = new Date(me.sharingPausedUntil).getTime(); expect(Math.abs(until - (Date.now() + ms))).toBeLessThan(5000); }); it('turns what was playing into "played … ago" immediately', async () => { const { token, user } = await createUser(app(), 'alice'); await applyPollResult(user.id, 'spotify', { track: { title: 'Ceremony', artist: 'New Order' }, isPlaying: true, progressMs: 0 }); await setSharing(token, 'indefinitely'); expect((await app().inject({ url: '/users/alice' })).json().nowPlaying.isPlaying).toBe(false); }); it('drops phone reports and records no history while paused, then shares again on resume', async () => { const { token } = await createUser(app(), 'alice'); const deviceToken = await deviceFor(token); await setSharing(token, 'indefinitely'); expect((await report(deviceToken, 'Temptation')).statusCode).toBe(204); let profile = (await app().inject({ url: '/users/alice' })).json(); expect(profile.nowPlaying).toBeNull(); expect(profile.recent).toEqual([]); expect((await setSharing(token, null)).json().sharingPausedUntil).toBeNull(); await report(deviceToken, 'Temptation'); profile = (await app().inject({ url: '/users/alice' })).json(); expect(profile.nowPlaying).toMatchObject({ track: { title: 'Temptation' }, isPlaying: true }); }); it('resuming polls connections right away', async () => { const { token, user } = await createUser(app(), 'alice'); const conn = await upsertConnection({ userId: user.id, provider: 'lastfm', externalUserId: 'x', displayName: null }); await setSharing(token, 'indefinitely'); await redis.zadd('poll:due', Date.now() + 600_000, conn.id); await setSharing(token, null); expect(Number(await redis.zscore('poll:due', conn.id))).toBeLessThanOrEqual(Date.now()); }); it('an expired pause counts as sharing', async () => { const { token, user } = await createUser(app(), 'alice'); await db.update(schema.users).set({ sharingPausedUntil: new Date(Date.now() - 1000) }).where(eq(schema.users.id, user.id)); expect((await app().inject({ url: '/me', headers: bearer(token) })).json().sharingPausedUntil).toBeNull(); await report(await deviceFor(token), 'Blue Monday'); expect((await app().inject({ url: '/users/alice' })).json().nowPlaying.isPlaying).toBe(true); }); it('rejects unknown durations', async () => { const { token } = await createUser(app(), 'alice'); expect((await setSharing(token, 'forever')).statusCode).toBe(400); }); });