Files
root-org/src/lib/api/events.test.ts

411 lines
16 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest';
import {
fetchEvents,
fetchEvent,
fetchEventBySlug,
createEvent,
updateEvent,
deleteEvent,
fetchEventMembers,
addEventMember,
removeEventMember,
fetchEventRoles,
createEventRole,
updateEventRole,
deleteEventRole,
fetchEventDepartments,
createEventDepartment,
updateEventDepartment,
updateDepartmentPlannedBudget,
deleteEventDepartment,
assignMemberDepartment,
unassignMemberDepartment,
} from './events';
// ── Supabase mock builder ────────────────────────────────────────────────────
function mockChain(resolvedValue: { data: any; error: any }) {
const chain: any = {};
const methods = ['from', 'select', 'insert', 'upsert', 'update', 'delete', 'eq', 'in', 'like', 'order', '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;
}
// ── fetchEvents ──────────────────────────────────────────────────────────────
describe('fetchEvents', () => {
it('returns events with member counts', async () => {
const raw = [{ id: 'e1', name: 'Conf', event_members: [{ count: 5 }] }];
const sb = mockSupabase({ data: raw, error: null });
const result = await fetchEvents(sb, 'o1');
expect(result).toHaveLength(1);
expect(result[0].member_count).toBe(5);
});
it('returns empty array when null', async () => {
const sb = mockSupabase({ data: null, error: null });
expect(await fetchEvents(sb, 'o1')).toEqual([]);
});
it('throws on error', async () => {
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
await expect(fetchEvents(sb, 'o1')).rejects.toEqual({ message: 'fail' });
});
});
// ── fetchEvent ───────────────────────────────────────────────────────────────
describe('fetchEvent', () => {
it('returns event by id', async () => {
const event = { id: 'e1', name: 'Conf' };
const sb = mockSupabase({ data: event, error: null });
expect(await fetchEvent(sb, 'e1')).toEqual(event);
});
it('returns null when not found', async () => {
const sb = mockSupabase({ data: null, error: { code: 'PGRST116', message: 'not found' } });
expect(await fetchEvent(sb, 'e1')).toBeNull();
});
it('throws on other errors', async () => {
const sb = mockSupabase({ data: null, error: { code: '42000', message: 'fail' } });
await expect(fetchEvent(sb, 'e1')).rejects.toEqual({ code: '42000', message: 'fail' });
});
});
// ── fetchEventBySlug ─────────────────────────────────────────────────────────
describe('fetchEventBySlug', () => {
it('returns event by slug', async () => {
const event = { id: 'e1', slug: 'conf' };
const sb = mockSupabase({ data: event, error: null });
expect(await fetchEventBySlug(sb, 'o1', 'conf')).toEqual(event);
});
it('returns null when not found', async () => {
const sb = mockSupabase({ data: null, error: { code: 'PGRST116', message: 'not found' } });
expect(await fetchEventBySlug(sb, 'o1', 'nope')).toBeNull();
});
it('throws on other errors', async () => {
const sb = mockSupabase({ data: null, error: { code: '42000', message: 'fail' } });
await expect(fetchEventBySlug(sb, 'o1', 'x')).rejects.toEqual({ code: '42000', message: 'fail' });
});
});
// ── createEvent ──────────────────────────────────────────────────────────────
describe('createEvent', () => {
it('creates event with unique slug', async () => {
const event = { id: 'e1', name: 'Conf', slug: 'conf' };
// Two calls: slug check + insert
const chain: any = {};
const methods = ['from', 'select', 'insert', 'eq', 'like', 'order', 'single'];
for (const m of methods) {
chain[m] = vi.fn(() => chain);
}
let callIdx = 0;
chain.then = (resolve: any) => {
callIdx++;
if (callIdx === 1) return resolve({ data: [], error: null }); // no existing slugs
return resolve({ data: event, error: null });
};
chain.single = vi.fn(() => Promise.resolve({ data: event, error: null }));
const sb = { from: vi.fn(() => chain) } as any;
const result = await createEvent(sb, 'o1', 'u1', { name: 'Conf' });
expect(result).toEqual(event);
});
it('throws on insert error', async () => {
const chain: any = {};
const methods = ['from', 'select', 'insert', 'eq', 'like', 'order', 'single'];
for (const m of methods) {
chain[m] = vi.fn(() => chain);
}
let callIdx = 0;
chain.then = (resolve: any) => {
callIdx++;
if (callIdx === 1) return resolve({ data: [], error: null });
return resolve({ data: null, error: { message: 'fail' } });
};
chain.single = vi.fn(() => Promise.resolve({ data: null, error: { message: 'fail' } }));
const sb = { from: vi.fn(() => chain) } as any;
await expect(createEvent(sb, 'o1', 'u1', { name: 'X' })).rejects.toEqual({ message: 'fail' });
});
});
// ── updateEvent ──────────────────────────────────────────────────────────────
describe('updateEvent', () => {
it('updates and returns event', async () => {
const event = { id: 'e1', name: 'Updated' };
const sb = mockSupabase({ data: event, error: null });
expect(await updateEvent(sb, 'e1', { name: 'Updated' })).toEqual(event);
});
it('throws on error', async () => {
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
await expect(updateEvent(sb, 'e1', { name: 'X' })).rejects.toEqual({ message: 'fail' });
});
});
// ── deleteEvent ──────────────────────────────────────────────────────────────
describe('deleteEvent', () => {
it('deletes without error', async () => {
const sb = mockSupabase({ data: null, error: null });
await expect(deleteEvent(sb, 'e1')).resolves.toBeUndefined();
});
it('throws on error', async () => {
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
await expect(deleteEvent(sb, 'e1')).rejects.toEqual({ message: 'fail' });
});
});
// ── fetchEventMembers ────────────────────────────────────────────────────────
describe('fetchEventMembers', () => {
it('returns empty array when no members', async () => {
const sb = mockSupabase({ data: [], error: null });
expect(await fetchEventMembers(sb, 'e1')).toEqual([]);
});
it('returns members with profiles, roles, and departments', async () => {
const members = [{ id: 'm1', event_id: 'e1', user_id: 'u1', role_id: 'r1' }];
const profiles = [{ id: 'u1', email: 'a@b.com', full_name: 'Alice' }];
const roles = [{ id: 'r1', event_id: 'e1', name: 'Lead' }];
const memberDepts = [{ event_member_id: 'm1', department_id: 'd1' }];
const departments = [{ id: 'd1', event_id: 'e1', name: 'Logistics' }];
const chain: any = {};
const methods = ['from', 'select', 'eq', 'in', 'order'];
for (const m of methods) {
chain[m] = vi.fn(() => chain);
}
let callIdx = 0;
chain.then = (resolve: any) => {
callIdx++;
switch (callIdx) {
case 1: return resolve({ data: members, error: null }); // members
case 2: return resolve({ data: profiles, error: null }); // profiles
case 3: return resolve({ data: roles, error: null }); // roles
case 4: return resolve({ data: memberDepts, error: null }); // member-depts
case 5: return resolve({ data: departments, error: null }); // departments
default: return resolve({ data: [], error: null });
}
};
const sb = { from: vi.fn(() => chain) } as any;
const result = await fetchEventMembers(sb, 'e1');
expect(result).toHaveLength(1);
expect(result[0].profile?.full_name).toBe('Alice');
expect(result[0].event_role?.name).toBe('Lead');
expect(result[0].departments).toHaveLength(1);
expect(result[0].departments[0].name).toBe('Logistics');
});
it('throws on error', async () => {
const chain: any = {};
const methods = ['from', 'select', 'eq', 'in', 'order'];
for (const m of methods) {
chain[m] = vi.fn(() => chain);
}
chain.then = (resolve: any) => resolve({ data: null, error: { message: 'fail' } });
const sb = { from: vi.fn(() => chain) } as any;
await expect(fetchEventMembers(sb, 'e1')).rejects.toEqual({ message: 'fail' });
});
});
// ── addEventMember ───────────────────────────────────────────────────────────
describe('addEventMember', () => {
it('adds member', async () => {
const member = { id: 'm1', event_id: 'e1', user_id: 'u1', role: 'member' };
const sb = mockSupabase({ data: member, error: null });
expect(await addEventMember(sb, 'e1', 'u1')).toEqual(member);
});
it('throws on error', async () => {
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
await expect(addEventMember(sb, 'e1', 'u1')).rejects.toEqual({ message: 'fail' });
});
});
// ── removeEventMember ────────────────────────────────────────────────────────
describe('removeEventMember', () => {
it('removes without error', async () => {
const sb = mockSupabase({ data: null, error: null });
await expect(removeEventMember(sb, 'e1', 'u1')).resolves.toBeUndefined();
});
it('throws on error', async () => {
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
await expect(removeEventMember(sb, 'e1', 'u1')).rejects.toEqual({ message: 'fail' });
});
});
// ── Event Roles ──────────────────────────────────────────────────────────────
describe('fetchEventRoles', () => {
it('returns roles', async () => {
const roles = [{ id: 'r1', name: 'Lead' }];
const sb = mockSupabase({ data: roles, error: null });
expect(await fetchEventRoles(sb, 'e1')).toEqual(roles);
});
it('throws on error', async () => {
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
await expect(fetchEventRoles(sb, 'e1')).rejects.toEqual({ message: 'fail' });
});
});
describe('createEventRole', () => {
it('creates role', async () => {
const role = { id: 'r1', name: 'Lead', color: '#6366f1' };
const sb = mockSupabase({ data: role, error: null });
expect(await createEventRole(sb, 'e1', { name: 'Lead' })).toEqual(role);
});
it('throws on error', async () => {
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
await expect(createEventRole(sb, 'e1', { name: 'X' })).rejects.toEqual({ message: 'fail' });
});
});
describe('updateEventRole', () => {
it('updates role', async () => {
const role = { id: 'r1', name: 'Updated' };
const sb = mockSupabase({ data: role, error: null });
expect(await updateEventRole(sb, 'r1', { name: 'Updated' })).toEqual(role);
});
it('throws on error', async () => {
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
await expect(updateEventRole(sb, 'r1', { name: 'X' })).rejects.toEqual({ message: 'fail' });
});
});
describe('deleteEventRole', () => {
it('deletes without error', async () => {
const sb = mockSupabase({ data: null, error: null });
await expect(deleteEventRole(sb, 'r1')).resolves.toBeUndefined();
});
it('throws on error', async () => {
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
await expect(deleteEventRole(sb, 'r1')).rejects.toEqual({ message: 'fail' });
});
});
// ── Event Departments ────────────────────────────────────────────────────────
describe('fetchEventDepartments', () => {
it('returns departments', async () => {
const depts = [{ id: 'd1', name: 'Logistics' }];
const sb = mockSupabase({ data: depts, error: null });
expect(await fetchEventDepartments(sb, 'e1')).toEqual(depts);
});
it('returns empty array when null', async () => {
const sb = mockSupabase({ data: null, error: null });
expect(await fetchEventDepartments(sb, 'e1')).toEqual([]);
});
it('throws on error', async () => {
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
await expect(fetchEventDepartments(sb, 'e1')).rejects.toEqual({ message: 'fail' });
});
});
describe('createEventDepartment', () => {
it('creates department', async () => {
const dept = { id: 'd1', name: 'Marketing', color: '#00A3E0' };
const sb = mockSupabase({ data: dept, error: null });
expect(await createEventDepartment(sb, 'e1', { name: 'Marketing' })).toEqual(dept);
});
it('throws on error', async () => {
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
await expect(createEventDepartment(sb, 'e1', { name: 'X' })).rejects.toEqual({ message: 'fail' });
});
});
describe('updateEventDepartment', () => {
it('updates department', async () => {
const dept = { id: 'd1', name: 'Updated' };
const sb = mockSupabase({ data: dept, error: null });
expect(await updateEventDepartment(sb, 'd1', { name: 'Updated' })).toEqual(dept);
});
it('throws on error', async () => {
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
await expect(updateEventDepartment(sb, 'd1', { name: 'X' })).rejects.toEqual({ message: 'fail' });
});
});
describe('updateDepartmentPlannedBudget', () => {
it('updates planned budget', async () => {
const dept = { id: 'd1', planned_budget: 5000 };
const sb = mockSupabase({ data: dept, error: null });
expect(await updateDepartmentPlannedBudget(sb, 'd1', 5000)).toEqual(dept);
});
it('throws on error', async () => {
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
await expect(updateDepartmentPlannedBudget(sb, 'd1', 0)).rejects.toEqual({ message: 'fail' });
});
});
describe('deleteEventDepartment', () => {
it('deletes without error', async () => {
const sb = mockSupabase({ data: null, error: null });
await expect(deleteEventDepartment(sb, 'd1')).resolves.toBeUndefined();
});
it('throws on error', async () => {
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
await expect(deleteEventDepartment(sb, 'd1')).rejects.toEqual({ message: 'fail' });
});
});
// ── Member-Department Assignments ────────────────────────────────────────────
describe('assignMemberDepartment', () => {
it('assigns member to department', async () => {
const md = { id: 'md1', event_member_id: 'm1', department_id: 'd1' };
const sb = mockSupabase({ data: md, error: null });
expect(await assignMemberDepartment(sb, 'm1', 'd1')).toEqual(md);
});
it('throws on error', async () => {
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
await expect(assignMemberDepartment(sb, 'm1', 'd1')).rejects.toEqual({ message: 'fail' });
});
});
describe('unassignMemberDepartment', () => {
it('unassigns without error', async () => {
const sb = mockSupabase({ data: null, error: null });
await expect(unassignMemberDepartment(sb, 'm1', 'd1')).resolves.toBeUndefined();
});
it('throws on error', async () => {
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
await expect(unassignMemberDepartment(sb, 'm1', 'd1')).rejects.toEqual({ message: 'fail' });
});
});