Add unit and integration test suites

- Shared: link building, feed ordering, input schemas, API client
- API unit: track matching, private-address blocking, token crypto,
  plan expiry, provider response parsing
- API integration against Postgres and Redis: auth, follows and pins,
  now-playing state, webhooks, OAuth return URLs, realtime, rate limits
- Tests use a dedicated *_test database and Redis DB, and refuse to wipe
  anything else; fetch is stubbed so they never hit the network
- Pin Node to 22.13+ (.nvmrc), which Vite's native bindings require

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
2026-10-03 11:28:17 -04:00
co-authored by Claude Opus 5.5
parent 95cd740e51
commit fe503307df
29 changed files with 3281 additions and 10 deletions
+23
View File
@@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest';
import { decrypt, encrypt, sha256 } from './crypto';
describe('token encryption', () => {
it('round-trips and uses a fresh IV each time', () => {
const a = encrypt('spotify-refresh-token');
const b = encrypt('spotify-refresh-token');
expect(a).not.toBe(b);
expect(decrypt(a)).toBe('spotify-refresh-token');
});
it('detects tampering', () => {
const buf = Buffer.from(encrypt('secret'), 'base64');
buf[buf.length - 1]! ^= 1;
expect(() => decrypt(buf.toString('base64'))).toThrow();
});
});
describe('sha256', () => {
it('is stable hex', () => {
expect(sha256('abc')).toBe('ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad');
});
});
+23
View File
@@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest';
import { PLAN_ENTITLEMENTS } from '@musicshare/shared';
import { effectivePlan, entitlementsFor } from './entitlements';
const day = 86_400_000;
describe('effectivePlan', () => {
it('honours an active paid plan', () => {
expect(effectivePlan({ plan: 'plus', planExpiresAt: new Date(Date.now() + day) })).toBe('plus');
});
it('falls back to free once the plan expires', () => {
expect(effectivePlan({ plan: 'plus', planExpiresAt: new Date(Date.now() - day) })).toBe('free');
});
it('treats a paid plan without an expiry as active (e.g. lifetime/comped)', () => {
expect(effectivePlan({ plan: 'plus', planExpiresAt: null })).toBe('plus');
});
it('maps to that plan’s entitlements', () => {
expect(entitlementsFor({ plan: 'free', planExpiresAt: null })).toBe(PLAN_ENTITLEMENTS.free);
});
});
+35
View File
@@ -0,0 +1,35 @@
import { describe, expect, it } from 'vitest';
import { matchKey, normalize, primaryArtist } from './http';
describe('normalize', () => {
it.each([
['Midnight City', 'midnight city'],
['Beyoncé', 'beyonce'],
['Hey Jude - Remastered 2015', 'hey jude'],
['Song (2011 Remaster)', 'song'],
['Song [Live]', 'song'],
['Song (feat. Someone)', 'song'],
['HUMBLE.', 'humble'],
])('%j → %j', (input, expected) => expect(normalize(input)).toBe(expected));
});
describe('primaryArtist', () => {
it.each([
['Daft Punk', 'Daft Punk'],
['Calvin Harris, Rihanna', 'Calvin Harris'],
['Simon & Garfunkel', 'Simon'],
['Drake feat. Rihanna', 'Drake'],
])('%j → %j', (input, expected) => expect(primaryArtist(input)).toBe(expected));
});
describe('matchKey', () => {
it('matches the same song reported differently by two services', () => {
expect(matchKey('Calvin Harris, Rihanna', 'This Is What You Came For')).toBe(
matchKey('Calvin Harris', 'This Is What You Came For (feat. Rihanna)'),
);
});
it('keeps different songs apart', () => {
expect(matchKey('M83', 'Midnight City')).not.toBe(matchKey('M83', 'Wait'));
});
});
+32
View File
@@ -0,0 +1,32 @@
import { describe, expect, it } from 'vitest';
import { UnsafeUrlError, assertSafeServerUrl } from './ssrf';
// ALLOW_PRIVATE_SERVER_URLS is false in the test env. IP literals avoid DNS.
describe('assertSafeServerUrl', () => {
it.each([
'http://127.0.0.1:8096',
'http://10.0.0.5',
'http://172.20.1.1',
'http://192.168.1.20',
'http://169.254.169.254/latest/meta-data', // cloud metadata
'http://100.64.0.1', // carrier-grade NAT
'http://0.0.0.0',
'http://[::1]',
'http://[fd00::1]',
'http://[fe80::1]',
'http://[::ffff:10.0.0.1]', // IPv4-mapped, normalized to hex by URL parsing
'http://[::ffff:a9fe:a9fe]', // mapped metadata IP in hex form
'http://[64:ff9b::a00:1]', // NAT64
])('blocks private address %s', async (url) => {
await expect(assertSafeServerUrl(url)).rejects.toBeInstanceOf(UnsafeUrlError);
});
it.each(['ftp://8.8.8.8', 'https://user:pass@8.8.8.8', 'not a url'])('rejects malformed URL %s', async (url) => {
await expect(assertSafeServerUrl(url)).rejects.toBeInstanceOf(UnsafeUrlError);
});
it('allows public addresses and normalizes trailing slashes', async () => {
await expect(assertSafeServerUrl('https://8.8.8.8/jellyfin/')).resolves.toBe('https://8.8.8.8/jellyfin');
await expect(assertSafeServerUrl('http://[2606:4700::1111]')).resolves.toBe('http://[2606:4700::1111]');
});
});
+35
View File
@@ -0,0 +1,35 @@
import { describe, expect, it } from 'vitest';
import { findBestMatch } from './match';
const target = { title: 'Midnight City', artist: 'M83', durationMs: 243_000 };
describe('findBestMatch', () => {
it('prefers the exact title with a matching duration', () => {
const best = findBestMatch(
[
{ id: 'remix', title: 'Midnight City (Eric Prydz Remix)', artist: 'M83', durationMs: 400_000 },
{ id: 'cover', title: 'Midnight City', artist: 'Party Machine', durationMs: 243_000 },
{ id: 'real', title: 'Midnight City', artist: 'M83', durationMs: 244_000 },
],
target,
);
expect(best?.id).toBe('real');
});
it('rejects other artists even with the same title', () => {
expect(findBestMatch([{ title: 'Midnight City', artist: 'Some Cover Band' }], target)).toBeNull();
});
it('rejects durations more than 15s apart', () => {
expect(findBestMatch([{ title: 'Midnight City', artist: 'M83', durationMs: 300_000 }], target)).toBeNull();
});
it('matches when either side lacks a duration', () => {
expect(findBestMatch([{ title: 'Midnight City', artist: 'M83' }], { title: 'Midnight City', artist: 'M83' })).not.toBeNull();
});
it('tolerates featured-artist and remaster suffixes', () => {
const best = findBestMatch([{ title: 'Midnight City - Remastered', artist: 'M83, Someone' }], target);
expect(best).not.toBeNull();
});
});
+161
View File
@@ -0,0 +1,161 @@
/**
* Provider response parsing, with fetch stubbed. These encode the quirks of
* each API (Last.fm's single-object responses, Jellyfin ticks, etc.).
*/
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { Connection } from '../db/schema';
import { encrypt } from '../lib/crypto';
import { jellyfinProvider } from './jellyfin';
import { lastfmProvider } from './lastfm';
import { listenbrainzProvider } from './listenbrainz';
import { ProviderAuthError } from './types';
const json = (body: unknown, status = 200) => new Response(JSON.stringify(body), { status });
function stubFetch(handler: (url: string, init?: RequestInit) => Response) {
const mock = vi.fn(async (input: string | URL | Request, init?: RequestInit) => handler(String(input), init));
vi.stubGlobal('fetch', mock);
return mock;
}
function conn(overrides: Partial<Connection>): Connection {
return {
id: 'c1',
userId: 'u1',
provider: 'lastfm',
externalUserId: 'someone',
displayName: null,
accessTokenEnc: null,
refreshTokenEnc: null,
tokenExpiresAt: null,
status: 'active',
lastError: null,
lastPolledAt: null,
meta: {},
webhookSecret: null,
createdAt: new Date(),
...overrides,
};
}
afterEach(() => vi.unstubAllGlobals());
describe('Last.fm', () => {
const lfmTrack = (nowPlaying: boolean) => ({
name: 'Teardrop',
artist: { '#text': 'Massive Attack' },
album: { '#text': 'Mezzanine' },
image: [{ size: 'extralarge', '#text': 'https://img/teardrop.jpg' }],
...(nowPlaying ? { '@attr': { nowplaying: 'true' } } : {}),
});
it('returns the now-playing track', async () => {
stubFetch(() => json({ recenttracks: { track: [lfmTrack(true), lfmTrack(false)] } }));
const result = await lastfmProvider.poll(conn({}));
expect(result).toEqual({
track: { title: 'Teardrop', artist: 'Massive Attack', album: 'Mezzanine', artworkUrl: 'https://img/teardrop.jpg' },
isPlaying: true,
progressMs: null,
});
});
it('handles Last.fm returning a single object instead of an array', async () => {
stubFetch(() => json({ recenttracks: { track: lfmTrack(true) } }));
expect((await lastfmProvider.poll(conn({})))?.track.title).toBe('Teardrop');
});
it('returns null when the latest scrobble is not playing now', async () => {
stubFetch(() => json({ recenttracks: { track: [lfmTrack(false)] } }));
expect(await lastfmProvider.poll(conn({}))).toBeNull();
});
it('drops the grey-star placeholder artwork', async () => {
const t = { ...lfmTrack(true), image: [{ size: 'extralarge', '#text': 'https://x/2a96cbd8b46e442fc41c2b86b821562f.png' }] };
stubFetch(() => json({ recenttracks: { track: [t] } }));
expect((await lastfmProvider.poll(conn({})))?.track.artworkUrl).toBeNull();
});
it('flags a deleted account as needing reconnect', async () => {
stubFetch(() => json({ error: 6, message: 'User not found' }, 404));
await expect(lastfmProvider.poll(conn({}))).rejects.toBeInstanceOf(ProviderAuthError);
});
});
describe('ListenBrainz', () => {
it('extracts ISRC, duration and a Spotify link when present', async () => {
stubFetch(() =>
json({
payload: {
listens: [
{
track_metadata: {
artist_name: 'Burial',
track_name: 'Archangel',
release_name: 'Untrue',
additional_info: { isrc: 'GBCFB0700013', duration_ms: 238000, spotify_id: 'https://open.spotify.com/track/6f6tQ' },
},
},
],
},
}),
);
const result = await listenbrainzProvider.poll(conn({ provider: 'listenbrainz' }));
expect(result?.track).toMatchObject({
title: 'Archangel',
isrc: 'GBCFB0700013',
durationMs: 238000,
links: { spotify: { id: '6f6tQ', url: 'https://open.spotify.com/track/6f6tQ' } },
});
});
it('returns null when nothing is playing', async () => {
stubFetch(() => json({ payload: { listens: [] } }));
expect(await listenbrainzProvider.poll(conn({ provider: 'listenbrainz' }))).toBeNull();
});
});
describe('Jellyfin', () => {
const jfConn = conn({
provider: 'jellyfin',
externalUserId: 'user-1',
accessTokenEnc: encrypt('jf-token'),
meta: { serverUrl: 'https://8.8.8.8', serverName: 'Home', deviceId: 'dev-1' },
});
const session = (userId: string, type: string, paused = false) => ({
UserId: userId,
NowPlayingItem: { Type: type, Name: 'Digital Love', Artists: ['Daft Punk'], Album: 'Discovery', RunTimeTicks: 3_010_000_000 },
PlayState: { IsPaused: paused, PositionTicks: 420_000_000 },
});
it("picks this user's audio session and converts ticks to ms", async () => {
const fetchMock = stubFetch(() => json([session('other', 'Audio'), session('user-1', 'Movie'), session('user-1', 'Audio')]));
const result = await jellyfinProvider.poll(jfConn);
expect(result).toEqual({
track: { title: 'Digital Love', artist: 'Daft Punk', album: 'Discovery', durationMs: 301_000 },
isPlaying: true,
progressMs: 42_000,
});
const [url, init] = fetchMock.mock.calls[0]!;
expect(url).toBe('https://8.8.8.8/Sessions?ActiveWithinSeconds=960');
expect((init?.headers as Record<string, string>).Authorization).toContain('Token="jf-token"');
expect(init?.redirect).toBe('error');
});
it('reports paused sessions as not playing', async () => {
stubFetch(() => json([session('user-1', 'Audio', true)]));
expect((await jellyfinProvider.poll(jfConn))?.isPlaying).toBe(false);
});
it('flags a revoked token as needing reconnect', async () => {
stubFetch(() => json({}, 401));
await expect(jellyfinProvider.poll(jfConn)).rejects.toBeInstanceOf(ProviderAuthError);
});
it('refuses servers on private networks', async () => {
const fetchMock = stubFetch(() => json([]));
await expect(jellyfinProvider.poll({ ...jfConn, meta: { ...jfConn.meta, serverUrl: 'http://192.168.1.5:8096' } })).rejects.toThrow(
/private network/,
);
expect(fetchMock).not.toHaveBeenCalled();
});
});