import { Hono } from 'hono'; import { beforeEach, describe, expect, it, vi } from 'vitest'; const { db } = vi.hoisted(() => { return { db: { group: { findMany: vi.fn(), findFirst: vi.fn(), count: vi.fn(), create: vi.fn(), }, groupMember: { create: vi.fn(), }, organization: { findUnique: vi.fn(), }, } as { group: { findMany: ReturnType; findFirst: ReturnType; count: ReturnType; create: ReturnType; }; groupMember: { create: ReturnType }; organization: { findUnique: ReturnType }; }, }; }); vi.mock('@/lib/prisma', () => ({ default: db, getPrismaClient: vi.fn(), UnitOfWork: class { executeResult = vi.fn( async (cb: (tx: unknown) => Promise) => cb(db), ); execute = vi.fn( 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'), }; 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 () => { vi.mocked(prisma.group.findMany).mockResolvedValue([group]); vi.mocked(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' }], }; vi.mocked(prisma.organization.findUnique).mockResolvedValue(organization as never); vi.mocked(prisma.group.findFirst).mockResolvedValue(null); vi.mocked(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' }], }; vi.mocked(prisma.organization.findUnique).mockResolvedValue(organization as never); vi.mocked(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 () => { vi.mocked(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' }], }; vi.mocked(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); }); });