import { beforeEach, describe, expect, it, mock, vi } from 'bun:test'; import { Hono } from 'hono'; const db = { group: { findUnique: mock(), findFirst: mock(), update: mock(), count: mock(), }, attendee: { findMany: mock(), findFirst: mock(), create: mock(), count: mock(), }, }; mock.module('@/lib/prisma', () => ({ default: db, getPrismaClient: mock(), UnitOfWork: class {}, })); import prisma from '@/lib/prisma'; import { groupsRoutes } from '@/modules/groups'; const teacherId = 'teacher-1'; const mockGroup = { id: 'group-1', name: 'Taller de Pintura', description: 'Acuarelas para principiantes', createdById: teacherId, inviteToken: 'pintura-token-xyz', createdAt: new Date('2026-09-01T10:00:00.000Z'), updatedAt: new Date('2026-09-01T10:00:00.000Z'), days: ['MONDAY'], time: '18:00', capacity: 20, price: 1500, billingType: 'MONTHLY', dueDay: 10, members: [], }; function makeApp(userValue: unknown) { const app = new Hono(); app.use('*', async (c, next) => { c.set('user', userValue as never); await next(); }); app.route('/groups', groupsRoutes); return app; } describe('attendee incorporation & invite-token in groups', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('GET /groups/:groupId/invite-token', () => { it('returns existing invite token and url', async () => { prisma.group.findUnique.mockResolvedValue(mockGroup as never); const app = makeApp({ id: teacherId }); const res = await app.request('/groups/group-1/invite-token'); expect(res.status).toBe(200); const data = await res.json(); expect(data.token).toBe('pintura-token-xyz'); expect(data.inviteUrl).toContain('/join/pintura-token-xyz'); }); it('generates a new token if group did not have one', async () => { prisma.group.findUnique.mockResolvedValue({ ...mockGroup, inviteToken: null, } as never); prisma.group.update.mockResolvedValue({} as never); const app = makeApp({ id: teacherId }); const res = await app.request('/groups/group-1/invite-token'); expect(res.status).toBe(200); const data = await res.json(); expect(data.token).toBeDefined(); expect(data.token.length).toBeGreaterThan(5); expect(prisma.group.update).toHaveBeenCalled(); }); it('rejects user without access to the group', async () => { prisma.group.findUnique.mockResolvedValue(mockGroup as never); const app = makeApp({ id: 'other-user' }); const res = await app.request('/groups/group-1/invite-token'); expect(res.status).toBe(403); }); }); describe('POST /groups/:groupId/attendees (individual quick add)', () => { it('creates an attendee for the group', async () => { prisma.group.findUnique.mockResolvedValue(mockGroup as never); prisma.attendee.findFirst.mockResolvedValue(null); prisma.attendee.create.mockResolvedValue({ id: 'att-1', groupId: 'group-1', fullName: 'Martín Gómez', email: 'martin@example.com', phone: '+5491133445566', guardianName: null, guardianPhone: null, notes: 'Viene con su hermano', createdAt: new Date('2026-09-18T10:00:00.000Z'), updatedAt: new Date('2026-09-18T10:00:00.000Z'), } as never); const app = makeApp({ id: teacherId }); const res = await app.request('/groups/group-1/attendees', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ firstName: 'Martín', lastName: 'Gómez', phone: '+54 9 11 3344-5566', email: 'martin@example.com', notes: 'Viene con su hermano', }), }); expect(res.status).toBe(201); const data = await res.json(); expect(data.fullName).toBe('Martín Gómez'); expect(data.phone).toBe('+5491133445566'); expect(prisma.attendee.create).toHaveBeenCalledWith({ data: { groupId: 'group-1', fullName: 'Martín Gómez', phone: '+5491133445566', email: 'martin@example.com', notes: 'Viene con su hermano', }, }); }); it('rejects duplicate phone number in individual add', async () => { prisma.group.findUnique.mockResolvedValue(mockGroup as never); prisma.attendee.findFirst.mockResolvedValue({ id: 'att-existing' } as never); const app = makeApp({ id: teacherId }); const res = await app.request('/groups/group-1/attendees', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ firstName: 'Martín', lastName: 'Gómez', phone: '1133445566', }), }); expect(res.status).toBe(409); expect(prisma.attendee.create).not.toHaveBeenCalled(); }); }); describe('POST /groups/:groupId/attendees/bulk (bulk import)', () => { it('imports multiple attendees, skipping duplicates and reporting counts', async () => { prisma.group.findUnique.mockResolvedValue(mockGroup as never); // Simular que ya existe un alumno con teléfono '11111111' prisma.attendee.findMany.mockResolvedValue([ { phone: '11111111' }, ] as never); prisma.attendee.create.mockImplementation(async ({ data }: { data: any }) => ({ id: `att-${Math.random()}`, ...data, guardianName: null, guardianPhone: null, createdAt: new Date('2026-09-18T10:00:00.000Z'), updatedAt: new Date('2026-09-18T10:00:00.000Z'), })); const app = makeApp({ id: teacherId }); const res = await app.request('/groups/group-1/attendees/bulk', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ attendees: [ { firstName: 'Ana', lastName: 'García', phone: '22222222' }, { firstName: 'Pedro', lastName: 'López', phone: '33333333' }, { firstName: 'Duplicado', lastName: 'Existente', phone: '11111111' }, // Ya existe en BD { firstName: 'Duplicado', lastName: 'EnLote', phone: '22222222' }, // Duplicado dentro del lote ], }), }); expect(res.status).toBe(201); const data = await res.json(); expect(data.totalProcessed).toBe(4); expect(data.createdCount).toBe(2); expect(data.duplicatesCount).toBe(2); expect(data.message).toBe('2 alumnos agregados con éxito, 2 duplicados omitidos.'); expect(prisma.attendee.create).toHaveBeenCalledTimes(2); }); }); describe('GET /groups/:groupId/attendees', () => { it('lists attendees filtered by groupId', async () => { prisma.attendee.findMany.mockResolvedValue([ { id: 'att-1', groupId: 'group-1', fullName: 'Ana García', email: null, phone: '22222222', 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); prisma.attendee.count.mockResolvedValue(1); const app = makeApp({ id: teacherId }); const res = await app.request('/groups/group-1/attendees'); expect(res.status).toBe(200); const data = await res.json(); expect(data.data.length).toBe(1); expect(data.data[0].fullName).toBe('Ana García'); expect(prisma.attendee.findMany).toHaveBeenCalledWith({ where: { groupId: 'group-1' }, skip: 0, take: 10, orderBy: { createdAt: 'desc' }, }); }); }); });