/
opt
/
mawid
/
apps
/
api
/
src
/
tenancy
/
/opt/mawid/apps/api/src/tenancy
mkdir
upload
Name
Size
Mode
Actions
tenancy.spec.ts
3571
0644
edit
dl
rm
Edit:
/opt/mawid/apps/api/src/tenancy/tenancy.spec.ts
(3571B)
/** * Cross-tenant isolation test (PROJECT_PLAN §4 Phase 1 / §7.2): * a user from clinic A must never read or mutate clinic B data. * * Integration test — requires the docker-compose postgres on localhost:5432. */ import { BadRequestException, NotFoundException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { StaffService } from '../staff/staff.service'; import { ServicesService } from '../services/services.service'; import { ClinicService } from '../clinic/clinic.service'; process.env.DATABASE_URL ??= 'postgresql://mawid:mawid@localhost:5432/mawid'; describe('tenancy isolation', () => { const prisma = new PrismaService(); const staffService = new StaffService(prisma); const servicesService = new ServicesService(prisma); const clinicService = new ClinicService(prisma); let clinicA: string; let clinicB: string; let staffB: string; let serviceB: string; beforeAll(async () => { const a = await prisma.clinic.create({ data: { name: `tenancy-test-A-${Date.now()}`, phone: '+900000000001' }, }); const b = await prisma.clinic.create({ data: { name: `tenancy-test-B-${Date.now()}`, phone: '+900000000002' }, }); clinicA = a.id; clinicB = b.id; staffB = ( await prisma.staff.create({ data: { clinicId: clinicB, name: 'B Staff' } }) ).id; serviceB = ( await prisma.service.create({ data: { clinicId: clinicB, name: { ar: 'خدمة', tr: 'Hizmet', en: 'Service' }, durationMinutes: 30, }, }) ).id; }); afterAll(async () => { // Cascade deletes remove staff/services created above. await prisma.clinic.deleteMany({ where: { id: { in: [clinicA, clinicB] } } }); await prisma.$disconnect(); }); it('listing staff for clinic A excludes clinic B rows', async () => { const staff = await staffService.list(clinicA); expect(staff.map((s) => s.id)).not.toContain(staffB); }); it('clinic A cannot read clinic B staff by id', async () => { await expect(staffService.get(clinicA, staffB)).rejects.toThrow(NotFoundException); }); it('clinic A cannot update or delete clinic B staff', async () => { await expect(staffService.update(clinicA, staffB, { name: 'hijacked' })).rejects.toThrow( NotFoundException, ); await expect(staffService.remove(clinicA, staffB)).rejects.toThrow(NotFoundException); const untouched = await prisma.staff.findUnique({ where: { id: staffB } }); expect(untouched?.name).toBe('B Staff'); }); it('clinic A cannot read, update or delete clinic B services', async () => { await expect(servicesService.get(clinicA, serviceB)).rejects.toThrow(NotFoundException); await expect( servicesService.update(clinicA, serviceB, { durationMinutes: 5 }), ).rejects.toThrow(NotFoundException); await expect(servicesService.remove(clinicA, serviceB)).rejects.toThrow(NotFoundException); }); it('clinic A staff cannot be linked to clinic B services', async () => { await expect( staffService.create(clinicA, { name: 'A Staff', role: 'practitioner', workingHours: {}, serviceIds: [serviceB], active: true, }), ).rejects.toThrow(BadRequestException); }); it('clinic settings reads are scoped to the caller clinic', async () => { const clinic = await clinicService.get(clinicA); expect(clinic.id).toBe(clinicA); const other = await clinicService.get(clinicB); expect(other.id).toBe(clinicB); }); });
Save
cmd:
run