feat: map shapes, image persistence, grab tool, layer rename/delete, i18n, page metadata
This commit is contained in:
146
src/lib/api/document-locks.test.ts
Normal file
146
src/lib/api/document-locks.test.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
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
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user