import 'reflect-metadata'; import { type CanActivate, type ExecutionContext, type INestApplication } from '@nestjs/common'; import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify'; import { Test } from '@nestjs/testing'; import request from 'supertest'; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { AuthGuard } from '../auth/auth.guard.js'; import { TeamsController } from './teams.controller.js'; import { TeamsService } from './teams.service.js'; const teamAlpha = { id: 'team-alpha', name: 'Alpha' }; const teamBeta = { id: 'team-beta', name: 'Beta' }; // user-1 is a member of team-alpha only; admin-1 has role admin. let currentUser: { id: string; role?: string } = { id: 'user-1' }; const teamsServiceMock = { findAll: vi.fn(() => Promise.resolve([teamAlpha, teamBeta])), findAllForUser: vi.fn((userId: string) => Promise.resolve(userId === 'user-1' ? [teamAlpha] : []), ), findById: vi.fn((id: string) => Promise.resolve([teamAlpha, teamBeta].find((t) => t.id === id))), listMembers: vi.fn(() => Promise.resolve([{ teamId: 'team-alpha', userId: 'user-1' }])), isMember: vi.fn((teamId: string, userId: string) => Promise.resolve(teamId === 'team-alpha' && userId === 'user-1'), ), }; const authGuard: CanActivate = { canActivate(context: ExecutionContext): boolean { const requestContext = context .switchToHttp() .getRequest<{ user?: { id: string; role?: string } }>(); requestContext.user = currentUser; return true; }, }; describe('teams endpoints are scoped to membership', () => { let app: INestApplication; beforeAll(async () => { const moduleRef = await Test.createTestingModule({ controllers: [TeamsController], providers: [{ provide: TeamsService, useValue: teamsServiceMock }], }) .overrideGuard(AuthGuard) .useValue(authGuard) .compile(); app = moduleRef.createNestApplication(new FastifyAdapter()); await app.init(); await app.getHttpAdapter().getInstance().ready(); }); beforeEach(() => { currentUser = { id: 'user-1' }; vi.clearAllMocks(); }); afterAll(async () => { await app.close(); }); it('GET /api/teams returns only the teams the user belongs to', async () => { const response = await request(app.getHttpServer()).get('/api/teams'); expect(response.status).toBe(200); expect(response.body).toEqual([teamAlpha]); expect(teamsServiceMock.findAll).not.toHaveBeenCalled(); }); it('GET /api/teams returns every team for an admin', async () => { currentUser = { id: 'admin-1', role: 'admin' }; const response = await request(app.getHttpServer()).get('/api/teams'); expect(response.status).toBe(200); expect(response.body).toEqual([teamAlpha, teamBeta]); expect(teamsServiceMock.findAllForUser).not.toHaveBeenCalled(); }); it('GET /api/teams/:teamId returns 403 for a non-member', async () => { const response = await request(app.getHttpServer()).get('/api/teams/team-beta'); expect(response.status).toBe(403); }); it('GET /api/teams/:teamId returns 404 for a missing team', async () => { const response = await request(app.getHttpServer()).get('/api/teams/team-missing'); expect(response.status).toBe(404); }); it('GET /api/teams/:teamId returns the team for a member', async () => { const response = await request(app.getHttpServer()).get('/api/teams/team-alpha'); expect(response.status).toBe(200); expect(response.body).toEqual(teamAlpha); }); it('GET /api/teams/:teamId/members returns 403 for a non-member and members for a member', async () => { const denied = await request(app.getHttpServer()).get('/api/teams/team-beta/members'); expect(denied.status).toBe(403); expect(teamsServiceMock.listMembers).not.toHaveBeenCalled(); const allowed = await request(app.getHttpServer()).get('/api/teams/team-alpha/members'); expect(allowed.status).toBe(200); expect(allowed.body).toEqual([{ teamId: 'team-alpha', userId: 'user-1' }]); }); it('GET /api/teams/:teamId/members/:userId allows a self-lookup on any team', async () => { const response = await request(app.getHttpServer()).get('/api/teams/team-beta/members/user-1'); expect(response.status).toBe(200); expect(response.body).toEqual({ isMember: false }); }); it('GET /api/teams/:teamId/members/:userId denies looking up another user on a foreign team', async () => { const response = await request(app.getHttpServer()).get('/api/teams/team-beta/members/user-2'); expect(response.status).toBe(403); }); it('an admin can look up any membership', async () => { currentUser = { id: 'admin-1', role: 'admin' }; const response = await request(app.getHttpServer()).get('/api/teams/team-alpha/members/user-1'); expect(response.status).toBe(200); expect(response.body).toEqual({ isMember: true }); }); });