40227-vm/backend/src/services/zone-checkin.test.ts

157 lines
4.8 KiB
TypeScript

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');
});
});