fix: audit remediation — SSE user scoping, FK transactional safety, UI error handling

- H4: scoped SSE import progress to exact userId match; non-HTTP events excluded from all subscribers
- H2: moved PRAGMA foreign_key_check inside SQLite transaction before COMMIT; violations rollback preserving old tables
- M1: removed dead axios-style error branch from extractErrorMessage (interceptor already unwraps)
- M2: split handleSave try/catch — save errors vs reload errors shown distinctly
- M3: added provider field validation before AI config test request
- Added SSE scoping regression tests (import service + controller)
- Added FK check failure rollback test (database-migrations.spec)
- Updated controller spec expectations for userId parameter

Co-authored-by: Code Review <branch-review>
This commit is contained in:
2026-07-12 22:59:03 +08:00
parent b6fca99390
commit cc4f4dae4e
69 changed files with 6262 additions and 1980 deletions

View File

@@ -7,32 +7,59 @@ function permissionsFor(roleCode: string): { groups: string[]; extras: string[]
}
describe('preset role permissions', () => {
it('gives teachers explicit workspace permissions without class/schedule delete privileges', () => {
it('keeps teachers read-only in scheduling while preserving class attendance access', () => {
const teacher = permissionsFor('teacher');
expect(teacher.groups).toEqual(['notification', 'profile']);
expect(teacher.extras).toEqual(
expect.arrayContaining([
'student:view',
'class:view',
'teacher-workspace:view',
'schedule:view',
'attendance:view',
'attendance:create',
'attendance:export',
'attendance:self-edit',
]),
);
expect(teacher.extras).not.toEqual(
expect.arrayContaining(['class:delete', 'schedule:delete']),
expect(teacher.extras).not.toContain('schedule:create');
expect(teacher.extras).not.toContain('schedule:edit');
expect(teacher.extras).not.toContain('schedule:delete');
expect(teacher.extras).not.toContain('student:view');
expect(teacher.extras).not.toContain('class:view');
expect(teacher.extras).not.toContain('attendance:export');
});
it('gives academic administrators the complete teaching administration workflow', () => {
const academic = permissionsFor('academic');
expect(academic.groups).toEqual(
expect.arrayContaining(['student', 'class', 'schedule', 'attendance', 'classroom']),
);
expect(academic.extras).toEqual(expect.arrayContaining(['sync:read', 'sync:trigger']));
});
it('combines accommodation, expenses, bills and deposits in one operations role', () => {
const accommodation = permissionsFor('accommodation_operations');
expect(accommodation.groups).toEqual(
expect.arrayContaining(['room', 'occupancy', 'expense', 'bill', 'deposit']),
);
expect(accommodation.extras).toContain('student:basic-view');
});
it('keeps classroom rental operations separate from accommodation operations', () => {
const classroomOperations = permissionsFor('classroom_operations');
expect(classroomOperations.groups).toEqual(
expect.arrayContaining(['classroom', 'rental', 'organization']),
);
expect(classroomOperations.groups).not.toEqual(expect.arrayContaining(['room', 'deposit']));
});
it('limits system administrators to accounts, permissions, logs and integrations', () => {
const systemAdmin = permissionsFor('system_admin');
expect(systemAdmin.groups).toEqual(
expect.arrayContaining(['user', 'role', 'log', 'integration', 'sync', 'ai']),
);
expect(systemAdmin.groups).not.toEqual(
expect.arrayContaining(['student', 'schedule', 'attendance', 'expense']),
);
});
it('gives institution heads every read permission required by the classroom rental pages', () => {
const role = permissionsFor('institution_head');
expect(role.groups).toEqual(expect.arrayContaining(['classroom', 'rental', 'organization']));
});
it('keeps roles without dashboard access off the dashboard', () => {
expect(permissionsFor('teacher').groups).not.toContain('dashboard');
expect(permissionsFor('institution_head').groups).not.toContain('dashboard');
});
});