import { beforeEach, describe, expect, it, mock, vi } from 'bun:test'; import { Hono } from 'hono'; const db = { group: { findMany: mock(), findFirst: mock(), count: mock(), create: mock(), }, groupMember: { create: mock(), }, organization: { findUnique: mock(), }, }; mock.module('@/lib/prisma', () => ({ default: db, getPrismaClient: mock(), UnitOfWork: class { executeResult = mock(async (cb: (tx: unknown) => Promise) => cb(db)); execute = mock(async (cb: (tx: unknown) => Promise) => cb(db)); }, })); import prisma from '@/lib/prisma'; import { groupsRoutes } from '@/modules/groups'; const userId = 'user-1'; const group = { id: 'group-1', name: 'Cuadrilla Alfa', description: null, createdById: userId, createdAt: new Date('2026-08-01T10:00:00.000Z'), updatedAt: new Date('2026-08-01T10:00:00.000Z'), days: null, time: null, capacity: null, price: null, billingType: null, dueDay: null, inviteToken: null, }; 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('groups routes', () => { beforeEach(() => { vi.clearAllMocks(); }); it('lists groups scoped to the current user', async () => { prisma.group.findMany.mockResolvedValue([group]); prisma.group.count.mockResolvedValue(1); const res = await makeApp({ id: userId }).request('/groups'); expect(res.status).toBe(200); expect(await res.json()).toEqual({ data: [ { ...group, createdAt: group.createdAt.toISOString(), updatedAt: group.updatedAt.toISOString(), }, ], pagination: { page: 1, pageSize: 10, total: 1, totalPages: 1 }, }); const where = { OR: [{ createdById: userId }, { members: { some: { userId } } }], }; expect(prisma.group.findMany).toHaveBeenCalledWith({ where, skip: 0, take: 10, orderBy: { createdAt: 'desc' }, }); expect(prisma.group.count).toHaveBeenCalledWith({ where }); }); it('rejects listing groups without a session', async () => { const res = await makeApp(null).request('/groups'); expect(res.status).toBe(401); expect(await res.json()).toMatchObject({ code: 'unauthorized' }); }); it('rejects invalid pagination query parameters', async () => { const res = await makeApp({ id: userId }).request('/groups?pageSize=101'); expect(res.status).toBe(400); }); it('creates a group from an owned organization', async () => { const organization = { id: 'org-1', name: 'Escuela Alfa', members: [{ userId, role: 'owner' }], }; prisma.organization.findUnique.mockResolvedValue(organization as never); prisma.group.findFirst.mockResolvedValue(null); prisma.group.create.mockResolvedValue(group); const res = await makeApp({ id: userId }).request('/groups/from-organization', { method: 'POST', body: JSON.stringify({ organizationId: 'org-1' }), headers: { 'Content-Type': 'application/json' }, }); expect(res.status).toBe(201); expect(await res.json()).toEqual({ group: { ...group, createdAt: group.createdAt.toISOString(), updatedAt: group.updatedAt.toISOString(), }, alreadyExists: false, }); expect(prisma.group.create).toHaveBeenCalledWith({ data: { name: 'Escuela Alfa', createdById: userId }, }); }); it('reuses an existing group with the same name', async () => { const organization = { id: 'org-1', name: 'Escuela Alfa', members: [{ userId, role: 'owner' }], }; prisma.organization.findUnique.mockResolvedValue(organization as never); prisma.group.findFirst.mockResolvedValue(group); const res = await makeApp({ id: userId }).request('/groups/from-organization', { method: 'POST', body: JSON.stringify({ organizationId: 'org-1' }), headers: { 'Content-Type': 'application/json' }, }); expect(res.status).toBe(200); const body = await res.json(); expect(body.alreadyExists).toBe(true); expect(prisma.group.create).not.toHaveBeenCalled(); }); it('rejects creating a group for an unknown organization', async () => { prisma.organization.findUnique.mockResolvedValue(null); const res = await makeApp({ id: userId }).request('/groups/from-organization', { method: 'POST', body: JSON.stringify({ organizationId: 'missing' }), headers: { 'Content-Type': 'application/json' }, }); expect(res.status).toBe(404); expect(await res.json()).toMatchObject({ code: 'organization_not_found' }); expect(prisma.group.create).not.toHaveBeenCalled(); }); it('rejects creating a group when the user is not the owner', async () => { const organization = { id: 'org-1', name: 'Escuela Alfa', members: [{ userId: 'other-user', role: 'owner' }], }; prisma.organization.findUnique.mockResolvedValue(organization as never); const res = await makeApp({ id: userId }).request('/groups/from-organization', { method: 'POST', body: JSON.stringify({ organizationId: 'org-1' }), headers: { 'Content-Type': 'application/json' }, }); expect(res.status).toBe(403); expect(await res.json()).toMatchObject({ code: 'group_owner_required' }); expect(prisma.group.create).not.toHaveBeenCalled(); }); it('rejects creating a group without a session', async () => { const res = await makeApp(null).request('/groups/from-organization', { method: 'POST', body: JSON.stringify({ organizationId: 'org-1' }), headers: { 'Content-Type': 'application/json' }, }); expect(res.status).toBe(401); }); });