- Introduced GroupWaitlistEntry model in Prisma schema to manage waitlisted users for groups. - Implemented API route to add users to the group waitlist with validation. - Enhanced attendee creation logic to handle group capacity and waitlisting scenarios. - Added new problem builders for handling waitlist-related errors. - Updated frontend to support waitlist interactions, including modals for capacity warnings. - Created tests for waitlist functionality, ensuring proper handling of full groups and existing waitlist entries.
204 lines
6.1 KiB
TypeScript
204 lines
6.1 KiB
TypeScript
import { beforeEach, describe, expect, it, mock, vi } from 'bun:test';
|
|
import { Hono } from 'hono';
|
|
|
|
const db = {
|
|
group: {
|
|
findUnique: mock(),
|
|
},
|
|
attendee: {
|
|
findFirst: mock(),
|
|
create: mock(),
|
|
count: mock(),
|
|
},
|
|
groupWaitlistEntry: {
|
|
findFirst: mock(),
|
|
create: mock(),
|
|
},
|
|
};
|
|
|
|
mock.module('@/lib/prisma', () => ({
|
|
default: db,
|
|
getPrismaClient: mock(),
|
|
UnitOfWork: class {},
|
|
}));
|
|
|
|
import prisma from '@/lib/prisma';
|
|
import { invitationsRoutes } from '@/modules/invitations';
|
|
|
|
const app = new Hono();
|
|
app.route('/invitations', invitationsRoutes);
|
|
|
|
const mockGroup = {
|
|
id: 'group-1',
|
|
name: 'Taller de Cerámica',
|
|
description: 'Clases de cerámica los sábados',
|
|
days: ['SATURDAY'],
|
|
time: '10:00',
|
|
capacity: 15,
|
|
price: 500,
|
|
billingType: 'MONTHLY',
|
|
createdById: 'teacher-1',
|
|
owner: { name: 'Prof. Mario Rossi' },
|
|
};
|
|
|
|
describe('public invitations routes', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('GET /invitations/:token', () => {
|
|
it('returns group details for a valid invite token', async () => {
|
|
prisma.group.findUnique.mockResolvedValue(mockGroup as never);
|
|
|
|
const res = await app.request('/invitations/valid-token-123');
|
|
|
|
expect(res.status).toBe(200);
|
|
const data = await res.json();
|
|
expect(data).toEqual({
|
|
id: 'group-1',
|
|
name: 'Taller de Cerámica',
|
|
description: 'Clases de cerámica los sábados',
|
|
teacherName: 'Prof. Mario Rossi',
|
|
days: ['SATURDAY'],
|
|
time: '10:00',
|
|
capacity: 15,
|
|
price: 500,
|
|
billingType: 'MONTHLY',
|
|
});
|
|
expect(prisma.group.findUnique).toHaveBeenCalledWith({
|
|
where: { inviteToken: 'valid-token-123' },
|
|
include: { owner: { select: { name: true } } },
|
|
});
|
|
});
|
|
|
|
it('returns 404 when the invite token is invalid', async () => {
|
|
prisma.group.findUnique.mockResolvedValue(null);
|
|
|
|
const res = await app.request('/invitations/invalid-token');
|
|
|
|
expect(res.status).toBe(404);
|
|
const body = await res.json();
|
|
expect(body.title).toBe('Not Found');
|
|
});
|
|
});
|
|
|
|
describe('POST /invitations/:token/join', () => {
|
|
it('registers a new attendee successfully', async () => {
|
|
prisma.group.findUnique.mockResolvedValue(mockGroup as never);
|
|
prisma.attendee.findFirst.mockResolvedValue(null);
|
|
prisma.attendee.create.mockResolvedValue({
|
|
id: 'att-1',
|
|
groupId: 'group-1',
|
|
fullName: 'Lucía Méndez',
|
|
email: 'lucia@test.com',
|
|
phone: '+5491122334455',
|
|
guardianName: null,
|
|
guardianPhone: null,
|
|
notes: null,
|
|
createdAt: new Date('2026-09-18T10:00:00.000Z'),
|
|
updatedAt: new Date('2026-09-18T10:00:00.000Z'),
|
|
} as never);
|
|
|
|
const res = await app.request('/invitations/valid-token-123/join', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
firstName: 'Lucía',
|
|
lastName: 'Méndez',
|
|
phone: '+54 9 11 2233-4455',
|
|
email: 'lucia@test.com',
|
|
}),
|
|
});
|
|
|
|
expect(res.status).toBe(201);
|
|
const body = await res.json();
|
|
expect(body.message).toBe('Inscripción realizada con éxito');
|
|
expect(body.attendee.fullName).toBe('Lucía Méndez');
|
|
expect(prisma.attendee.create).toHaveBeenCalledWith({
|
|
data: {
|
|
groupId: 'group-1',
|
|
fullName: 'Lucía Méndez',
|
|
phone: '+5491122334455',
|
|
email: 'lucia@test.com',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('rejects registration when phone already exists in the group', async () => {
|
|
prisma.group.findUnique.mockResolvedValue(mockGroup as never);
|
|
prisma.attendee.findFirst.mockResolvedValue({ id: 'existing-att' } as never);
|
|
|
|
const res = await app.request('/invitations/valid-token-123/join', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
firstName: 'Lucía',
|
|
lastName: 'Méndez',
|
|
phone: '1122334455',
|
|
}),
|
|
});
|
|
|
|
expect(res.status).toBe(409);
|
|
const body = await res.json();
|
|
expect(body.code).toBe('attendee_already_registered');
|
|
expect(prisma.attendee.create).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('rejects invalid inputs (missing required name/phone)', async () => {
|
|
const res = await app.request('/invitations/valid-token-123/join', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
firstName: '',
|
|
phone: '12',
|
|
}),
|
|
});
|
|
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it('adds to the waitlist when the group is full', async () => {
|
|
prisma.group.findUnique.mockResolvedValue(mockGroup as never);
|
|
prisma.attendee.findFirst.mockResolvedValue(null);
|
|
prisma.attendee.count.mockResolvedValue(15); // capacity full
|
|
prisma.groupWaitlistEntry.findFirst.mockResolvedValue(null);
|
|
prisma.groupWaitlistEntry.create.mockResolvedValue({
|
|
id: 'wl-1',
|
|
groupId: 'group-1',
|
|
fullName: 'Lucía Méndez',
|
|
phone: '+5491122334455',
|
|
email: 'lucia@test.com',
|
|
notes: null,
|
|
status: 'PENDING',
|
|
createdAt: new Date('2026-09-18T10:00:00.000Z'),
|
|
updatedAt: new Date('2026-09-18T10:00:00.000Z'),
|
|
} as never);
|
|
|
|
const res = await app.request('/invitations/valid-token-123/join', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
firstName: 'Lucía',
|
|
lastName: 'Méndez',
|
|
phone: '+54 9 11 2233-4455',
|
|
email: 'lucia@test.com',
|
|
}),
|
|
});
|
|
|
|
expect(res.status).toBe(201);
|
|
const body = await res.json();
|
|
expect(body.status).toBe('waitlisted');
|
|
expect(body.attendee).toBeNull();
|
|
expect(prisma.attendee.create).not.toHaveBeenCalled();
|
|
expect(prisma.groupWaitlistEntry.create).toHaveBeenCalledWith({
|
|
data: {
|
|
groupId: 'group-1',
|
|
fullName: 'Lucía Méndez',
|
|
phone: '+5491122334455',
|
|
email: 'lucia@test.com',
|
|
},
|
|
});
|
|
});
|
|
});
|
|
});
|