import { afterEach, describe, mock, test } from 'node:test'; import assert from 'node:assert/strict'; import db from '@/db/models'; import ZoneCheckinService from '@/services/zone-checkin'; import UserProgressService from '@/services/user_progress'; import ForbiddenError from '@/shared/errors/forbidden'; import { FEATURE_PERMISSIONS } from '@/shared/constants/product-permissions'; import { ROLE_NAMES } from '@/shared/constants/roles'; import { localDateInTimezone } from '@/shared/constants/timezone'; import { createGlobalAccessUser, createTestUser } from '@/test-utils'; import type { CurrentUser } from '@/db/api/types'; function user(roleName: string): CurrentUser { return createTestUser({ campusId: 'campus-1', app_role: { name: roleName, globalAccess: false, permissions: [{ name: FEATURE_PERMISSIONS.ZONE_CHECKIN }], }, }); } afterEach(() => { mock.restoreAll(); }); describe('ZoneCheckinService role eligibility', () => { test('rejects global access without explicit ZONE_CHECKIN', async () => { const rejectedUsers = [ createGlobalAccessUser(), ]; for (const currentUser of rejectedUsers) { await assert.rejects( () => ZoneCheckinService.today(currentUser), ForbiddenError, ); } }); test('allows an explicit ZONE_CHECKIN permission regardless of role name', async () => { mock.method(db.campuses, 'findByPk', (async () => ({ timezone: 'America/Phoenix', })) as typeof db.campuses.findByPk); mock.method(UserProgressService, 'list', (async () => ({ rows: [], count: 0, })) as typeof UserProgressService.list); const result = await ZoneCheckinService.today(user(ROLE_NAMES.OWNER)); assert.equal(result.zone, null); assert.equal(result.isCheckedInToday, false); }); test('allows a campus workflow role to read today status', async () => { mock.method(db.campuses, 'findByPk', (async () => ({ timezone: 'America/Phoenix', })) as typeof db.campuses.findByPk); mock.method(UserProgressService, 'list', (async () => ({ rows: [], count: 0, })) as typeof UserProgressService.list); const result = await ZoneCheckinService.today(user(ROLE_NAMES.TEACHER)); assert.equal(result.zone, null); assert.equal(result.isCheckedInToday, false); assert.match(result.date, /^\d{4}-\d{2}-\d{2}$/); }); test('blocks parent-drill zone mutations', async () => { const currentUser = createTestUser({ organizationId: 'org-1', organizations: { id: 'org-1' }, campusId: null, app_role: { name: ROLE_NAMES.OWNER, scope: 'organization', globalAccess: false, permissions: [{ name: FEATURE_PERMISSIONS.ZONE_CHECKIN }], }, activeScope: { level: 'campus', organizationId: 'org-1', schoolId: 'school-1', campusId: 'campus-1', classId: null, }, }); await assert.rejects( () => ZoneCheckinService.checkIn({ zone: 'green' }, currentUser), ForbiddenError, ); await assert.rejects( () => ZoneCheckinService.clearToday(currentUser), ForbiddenError, ); }); }); describe('ZoneCheckinService completion report', () => { test('builds scoped daily rows and counts non-green zones', async () => { const currentUser = createTestUser({ organizationId: 'org-1', app_role: { name: ROLE_NAMES.DIRECTOR, scope: 'campus', globalAccess: false, permissions: [{ name: FEATURE_PERMISSIONS.READ_ZONE_CHECKIN_REPORTS }], }, campusId: 'campus-1', }); mock.method(db.users, 'findAll', (async () => [ { id: 'user-1', firstName: 'Ava', lastName: 'Lee', email: 'ava@example.test', campusId: 'campus-1', app_role: { name: ROLE_NAMES.TEACHER }, }, { id: 'user-2', firstName: 'Ben', lastName: 'Stone', email: 'ben@example.test', campusId: 'campus-1', app_role: { name: ROLE_NAMES.SUPPORT_STAFF }, }, ]) as typeof db.users.findAll); mock.method(db.campuses, 'findAll', (async () => [ { id: 'campus-1', timezone: 'UTC' }, ]) as typeof db.campuses.findAll); mock.method(db.user_progress, 'findAll', (async () => [ { userId: 'user-1', item_id: localDateInTimezone('UTC'), value: 'yellow', }, ]) as typeof db.user_progress.findAll); const report = await ZoneCheckinService.completion(currentUser); assert.equal(report.summary.totalStaff, 2); assert.equal(report.summary.completedCount, 1); assert.equal(report.summary.pendingCount, 1); assert.equal(report.summary.nonGreenCount, 1); assert.equal(report.rows[0].result, 'Yellow Zone'); assert.equal(report.rows[0].riskLevel, 'medium'); assert.equal(report.rows[1].status, 'pending'); }); });