mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 23:35:44 +00:00
refactor(orbit): co-locate orbit feature into features/orbit
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
ORBIT_PLAYLIST_PREFIX,
|
||||
makeInitialOrbitState,
|
||||
orbitOutboxPlaylistName,
|
||||
orbitSessionPlaylistName,
|
||||
type OrbitState,
|
||||
} from '@/features/orbit/api/orbit';
|
||||
import { ORBIT_ORPHAN_TTL_MS } from '@/features/orbit/utils/constants';
|
||||
|
||||
const { getPlaylists, deletePlaylist } = vi.hoisted(() => ({
|
||||
getPlaylists: vi.fn(),
|
||||
deletePlaylist: vi.fn(),
|
||||
}));
|
||||
|
||||
const { authState, orbitState } = vi.hoisted(() => ({
|
||||
authState: { username: 'me' as string | undefined },
|
||||
orbitState: { sessionId: null as string | null },
|
||||
}));
|
||||
|
||||
vi.mock('@/api/subsonicPlaylists', () => ({ getPlaylists, deletePlaylist }));
|
||||
vi.mock('@/store/authStore', () => ({
|
||||
useAuthStore: {
|
||||
getState: () => ({
|
||||
getActiveServer: () => (authState.username ? { username: authState.username } : undefined),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
vi.mock('@/features/orbit/store/orbitStore', () => ({
|
||||
useOrbitStore: { getState: () => ({ sessionId: orbitState.sessionId }) },
|
||||
}));
|
||||
|
||||
import { cleanupOrphanedOrbitPlaylists } from '@/features/orbit/utils/cleanup';
|
||||
|
||||
type FakePlaylist = {
|
||||
id: string;
|
||||
name: string;
|
||||
owner?: string;
|
||||
comment?: string;
|
||||
changed?: string;
|
||||
};
|
||||
|
||||
/** A session-playlist comment whose heartbeat (`positionAt`) is `ageMs` old. */
|
||||
function sessionComment(sid: string, ageMs: number, ended = false): string {
|
||||
const state: OrbitState = makeInitialOrbitState({ sid, host: 'me', name: 'sesh' });
|
||||
state.positionAt = Date.now() - ageMs;
|
||||
if (ended) state.ended = true;
|
||||
return JSON.stringify(state);
|
||||
}
|
||||
|
||||
/** An outbox comment whose heartbeat `ts` is `ageMs` old. */
|
||||
function outboxComment(ageMs: number): string {
|
||||
return JSON.stringify({ ts: Date.now() - ageMs });
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
authState.username = 'me';
|
||||
orbitState.sessionId = null;
|
||||
deletePlaylist.mockReset().mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
getPlaylists.mockReset();
|
||||
});
|
||||
|
||||
async function runWith(playlists: FakePlaylist[]): Promise<string[]> {
|
||||
getPlaylists.mockResolvedValue(playlists);
|
||||
await cleanupOrphanedOrbitPlaylists();
|
||||
return deletePlaylist.mock.calls.map(c => c[0] as string);
|
||||
}
|
||||
|
||||
describe('cleanupOrphanedOrbitPlaylists', () => {
|
||||
it('keeps a fresh session playlist from another device (regression for the regex bug)', async () => {
|
||||
const sid = 'aaaa1111';
|
||||
const deleted = await runWith([
|
||||
{ id: 'p1', name: orbitSessionPlaylistName(sid), owner: 'me', comment: sessionComment(sid, 1_000) },
|
||||
]);
|
||||
expect(deleted).toEqual([]);
|
||||
});
|
||||
|
||||
it('deletes a stale session playlist past the orphan TTL', async () => {
|
||||
const sid = 'bbbb2222';
|
||||
const deleted = await runWith([
|
||||
{
|
||||
id: 'p2',
|
||||
name: orbitSessionPlaylistName(sid),
|
||||
owner: 'me',
|
||||
comment: sessionComment(sid, ORBIT_ORPHAN_TTL_MS + 60_000),
|
||||
},
|
||||
]);
|
||||
expect(deleted).toEqual(['p2']);
|
||||
});
|
||||
|
||||
it('deletes a session the host explicitly ended even when fresh', async () => {
|
||||
const sid = 'cccc3333';
|
||||
const deleted = await runWith([
|
||||
{
|
||||
id: 'p3',
|
||||
name: orbitSessionPlaylistName(sid),
|
||||
owner: 'me',
|
||||
comment: sessionComment(sid, 1_000, /* ended */ true),
|
||||
},
|
||||
]);
|
||||
expect(deleted).toEqual(['p3']);
|
||||
});
|
||||
|
||||
it('never touches this device\'s current session', async () => {
|
||||
const sid = 'dddd4444';
|
||||
orbitState.sessionId = sid;
|
||||
const deleted = await runWith([
|
||||
// Stale heartbeat, but it's *our* live session → must be skipped.
|
||||
{
|
||||
id: 'p4',
|
||||
name: orbitSessionPlaylistName(sid),
|
||||
owner: 'me',
|
||||
comment: sessionComment(sid, ORBIT_ORPHAN_TTL_MS + 60_000),
|
||||
},
|
||||
]);
|
||||
expect(deleted).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps a fresh outbox but prunes a stale one', async () => {
|
||||
const sid = 'eeee5555';
|
||||
const deleted = await runWith([
|
||||
{ id: 'fresh', name: orbitOutboxPlaylistName(sid, 'bob'), owner: 'me', comment: outboxComment(1_000) },
|
||||
{
|
||||
id: 'stale',
|
||||
name: orbitOutboxPlaylistName(sid, 'eve'),
|
||||
owner: 'me',
|
||||
comment: outboxComment(ORBIT_ORPHAN_TTL_MS + 60_000),
|
||||
},
|
||||
]);
|
||||
expect(deleted).toEqual(['stale']);
|
||||
});
|
||||
|
||||
it('prunes an unrecognisable __psyorbit_* playlist', async () => {
|
||||
const deleted = await runWith([
|
||||
{ id: 'junk', name: `${ORBIT_PLAYLIST_PREFIX}not-a-real-name`, owner: 'me' },
|
||||
]);
|
||||
expect(deleted).toEqual(['junk']);
|
||||
});
|
||||
|
||||
it('ignores playlists owned by another user', async () => {
|
||||
const sid = 'ffff6666';
|
||||
const deleted = await runWith([
|
||||
{
|
||||
id: 'foreign',
|
||||
name: orbitSessionPlaylistName(sid),
|
||||
owner: 'someone-else',
|
||||
comment: sessionComment(sid, ORBIT_ORPHAN_TTL_MS + 60_000),
|
||||
},
|
||||
]);
|
||||
expect(deleted).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user