feat(P5A.T2): WatchTogetherPayload + wrappers + parseYouTubeUrl + tests

This commit is contained in:
byGalax
2026-05-16 21:21:30 +02:00
parent ad239ec549
commit 4399d39f08
5 changed files with 298 additions and 1 deletions
@@ -0,0 +1,124 @@
import { describe, expect, it, vi } from 'vitest';
import {
createWatchSession,
getWatchSession,
parseYouTubeUrl,
updateWatchSessionState,
} from './watchTogether';
describe('parseYouTubeUrl', () => {
it.each([
['https://www.youtube.com/watch?v=dQw4w9WgXcQ', 'dQw4w9WgXcQ'],
['https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=42', 'dQw4w9WgXcQ'],
['https://youtu.be/dQw4w9WgXcQ', 'dQw4w9WgXcQ'],
['https://youtu.be/dQw4w9WgXcQ?t=1', 'dQw4w9WgXcQ'],
['https://www.youtube.com/embed/dQw4w9WgXcQ', 'dQw4w9WgXcQ'],
['https://www.youtube.com/shorts/dQw4w9WgXcQ', 'dQw4w9WgXcQ'],
['https://m.youtube.com/watch?v=dQw4w9WgXcQ', 'dQw4w9WgXcQ'],
['dQw4w9WgXcQ', 'dQw4w9WgXcQ'],
])('extracts the id from %s', (url, expected) => {
expect(parseYouTubeUrl(url)).toBe(expected);
});
it.each([
'',
' ',
'https://vimeo.com/123',
'not a url',
'short_id',
'https://www.youtube.com/playlist?list=PL123',
])('returns null for %s', (input) => {
expect(parseYouTubeUrl(input)).toBeNull();
});
});
function makeClient(opts: {
user?: { id: string } | null;
insertReturn?: { data: unknown; error: unknown };
selectReturn?: { data: unknown; error: unknown };
updateReturn?: { error: unknown };
}): any {
const single = vi.fn().mockResolvedValue(opts.insertReturn ?? { data: {}, error: null });
const insertSelect = vi.fn().mockReturnValue({ single });
const insertChain = vi.fn().mockReturnValue({ select: insertSelect });
const maybeSingle = vi.fn().mockResolvedValue(opts.selectReturn ?? { data: null, error: null });
const eqSelect = vi.fn().mockReturnValue({ maybeSingle });
const selectChain = vi.fn().mockReturnValue({ eq: eqSelect });
const eqUpdate = vi.fn().mockResolvedValue(opts.updateReturn ?? { error: null });
const updateChain = vi.fn().mockReturnValue({ eq: eqUpdate });
const from = vi.fn().mockReturnValue({
insert: insertChain,
select: selectChain,
update: updateChain,
});
return {
auth: { getUser: vi.fn().mockResolvedValue({ data: { user: opts.user ?? { id: 'u-1' } } }) },
from,
};
}
describe('createWatchSession', () => {
it('inserts row + maps response to camelCase', async () => {
const client = makeClient({
insertReturn: {
data: {
id: 'w-1',
conversation_id: 'c-1',
owner_user_id: 'u-1',
video_id: 'dQw4w9WgXcQ',
started_at: '2026-05-16T00:00:00Z',
ended_at: null,
current_state: { playing: false, position_seconds: 0, updated_at_ms: 0 },
},
error: null,
},
});
const out = await createWatchSession(client, { conversationId: 'c-1', videoId: 'dQw4w9WgXcQ' });
expect(out.id).toBe('w-1');
expect(out.ownerUserId).toBe('u-1');
expect(out.videoId).toBe('dQw4w9WgXcQ');
expect(out.endedAt).toBeNull();
expect(out.currentState.playing).toBe(false);
});
});
describe('getWatchSession', () => {
it('returns null when row not found', async () => {
const client = makeClient({ selectReturn: { data: null, error: null } });
const out = await getWatchSession(client, 'w-missing');
expect(out).toBeNull();
});
it('coerces missing current_state fields to safe defaults', async () => {
const client = makeClient({
selectReturn: {
data: {
id: 'w-1',
conversation_id: 'c-1',
owner_user_id: 'u-1',
video_id: 'dQw4w9WgXcQ',
started_at: '2026-05-16T00:00:00Z',
ended_at: null,
current_state: {},
},
error: null,
},
});
const out = await getWatchSession(client, 'w-1');
expect(out?.currentState).toEqual({ playing: false, positionSeconds: 0, updatedAtMs: 0 });
});
});
describe('updateWatchSessionState', () => {
it('does not throw on success', async () => {
const client = makeClient({ updateReturn: { error: null } });
await expect(
updateWatchSessionState(client, 'w-1', {
playing: true,
positionSeconds: 42.5,
updatedAtMs: Date.now(),
}),
).resolves.toBeUndefined();
});
});