411 lines
16 KiB
TypeScript
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' });
|
|
});
|
|
});
|