157 lines
4.8 KiB
TypeScript
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');
|
|
});
|
|
});
|