import { describe, it, expect, vi } from 'vitest'; import { getLockInfo, acquireLock, heartbeatLock, releaseLock, startHeartbeat, } from './document-locks'; // ── Supabase mock builder ──────────────────────────────────────────────────── function mockChain(resolvedValue: { data: any; error: any }) { const chain: any = {}; const methods = ['from', 'select', 'insert', 'update', 'delete', 'eq', 'gt', 'lt', 'single']; for (const m of methods) { chain[m] = vi.fn(() => chain); } chain.single = vi.fn(() => Promise.resolve(resolvedValue)); chain.then = (resolve: any) => resolve(resolvedValue); return chain; } function mockSupabase(resolvedValue: { data: any; error: any }) { const chain = mockChain(resolvedValue); return { from: vi.fn(() => chain), _chain: chain } as any; } // ── getLockInfo ────────────────────────────────────────────────────────────── describe('getLockInfo', () => { it('returns unlocked when no lock exists', async () => { const sb = mockSupabase({ data: null, error: null }); const info = await getLockInfo(sb, 'doc1', 'user1'); expect(info).toEqual({ isLocked: false, lockedBy: null, lockedByName: null, isOwnLock: false, }); }); it('returns locked with own lock', async () => { // First call returns lock, second returns profile const lockData = { id: 'l1', document_id: 'doc1', user_id: 'user1', locked_at: new Date().toISOString(), last_heartbeat: new Date().toISOString() }; const profileData = { full_name: 'Alice', email: 'alice@test.com' }; const chain: any = {}; const methods = ['from', 'select', 'eq', 'gt', 'single']; for (const m of methods) { chain[m] = vi.fn(() => chain); } let callCount = 0; chain.single = vi.fn(() => { callCount++; if (callCount === 1) return Promise.resolve({ data: lockData, error: null }); return Promise.resolve({ data: profileData, error: null }); }); const sb = { from: vi.fn(() => chain) } as any; const info = await getLockInfo(sb, 'doc1', 'user1'); expect(info.isLocked).toBe(true); expect(info.isOwnLock).toBe(true); expect(info.lockedByName).toBe('Alice'); }); it('returns locked with other user lock', async () => { const lockData = { id: 'l1', document_id: 'doc1', user_id: 'user2', locked_at: new Date().toISOString(), last_heartbeat: new Date().toISOString() }; const profileData = { full_name: 'Bob', email: 'bob@test.com' }; const chain: any = {}; const methods = ['from', 'select', 'eq', 'gt', 'single']; for (const m of methods) { chain[m] = vi.fn(() => chain); } let callCount = 0; chain.single = vi.fn(() => { callCount++; if (callCount === 1) return Promise.resolve({ data: lockData, error: null }); return Promise.resolve({ data: profileData, error: null }); }); const sb = { from: vi.fn(() => chain) } as any; const info = await getLockInfo(sb, 'doc1', 'user1'); expect(info.isLocked).toBe(true); expect(info.isOwnLock).toBe(false); expect(info.lockedByName).toBe('Bob'); }); }); // ── acquireLock ────────────────────────────────────────────────────────────── describe('acquireLock', () => { it('returns true when lock acquired', async () => { const sb = mockSupabase({ data: null, error: null }); expect(await acquireLock(sb, 'doc1', 'user1')).toBe(true); }); it('returns false on unique constraint violation', async () => { const sb = mockSupabase({ data: null, error: { code: '23505', message: 'unique' } }); expect(await acquireLock(sb, 'doc1', 'user1')).toBe(false); }); it('returns false on other errors', async () => { const sb = mockSupabase({ data: null, error: { code: '42000', message: 'other' } }); expect(await acquireLock(sb, 'doc1', 'user1')).toBe(false); }); }); // ── heartbeatLock ──────────────────────────────────────────────────────────── describe('heartbeatLock', () => { it('returns true on success', async () => { const sb = mockSupabase({ data: null, error: null }); expect(await heartbeatLock(sb, 'doc1', 'user1')).toBe(true); }); it('returns false on error', async () => { const sb = mockSupabase({ data: null, error: { message: 'fail' } }); expect(await heartbeatLock(sb, 'doc1', 'user1')).toBe(false); }); }); // ── releaseLock ────────────────────────────────────────────────────────────── describe('releaseLock', () => { it('releases without error', async () => { const sb = mockSupabase({ data: null, error: null }); await expect(releaseLock(sb, 'doc1', 'user1')).resolves.toBeUndefined(); }); it('does not throw on error (just logs)', async () => { const sb = mockSupabase({ data: null, error: { message: 'fail' } }); await expect(releaseLock(sb, 'doc1', 'user1')).resolves.toBeUndefined(); }); }); // ── startHeartbeat ─────────────────────────────────────────────────────────── describe('startHeartbeat', () => { it('returns a cleanup function', () => { const sb = mockSupabase({ data: null, error: null }); const cleanup = startHeartbeat(sb, 'doc1', 'user1'); expect(typeof cleanup).toBe('function'); cleanup(); // should not throw }); });