forked from wangziqi/gongxue-base
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:
@@ -125,4 +125,101 @@ describe('AttendanceImportService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it('auto-matches previously imported duplicate records', async () => {
|
||||
dingTalkService.fetchAttendanceResults.mockResolvedValue([
|
||||
{
|
||||
userId: 'ding-1',
|
||||
userName: '张三',
|
||||
workDate: '2026-07-01',
|
||||
timeResult: 'Normal',
|
||||
locationResult: '',
|
||||
planCheckTime: '',
|
||||
actualCheckTime: '2026-07-01T08:00:00.000Z',
|
||||
checkId: 'check-1',
|
||||
checkType: 'OnDuty',
|
||||
},
|
||||
]);
|
||||
dingRawRepo.find.mockResolvedValue([{ dingId: 'check-1' }]);
|
||||
attendanceService.autoMatchDingRecords.mockResolvedValue({ matched: 1, total: 1 });
|
||||
|
||||
const result = await service.importFromDingTalk({
|
||||
startDate: '2026-07-01',
|
||||
endDate: '2026-07-01',
|
||||
userIds: ['ding-1'],
|
||||
autoMatch: true,
|
||||
});
|
||||
|
||||
expect(attendanceService.autoMatchDingRecords).toHaveBeenCalled();
|
||||
expect(result.matched).toBe(1);
|
||||
});
|
||||
|
||||
it('scopes SSE progress events to the importing user', async () => {
|
||||
dingTalkService.fetchAttendanceResults.mockResolvedValue([
|
||||
{
|
||||
userId: 'ding-1',
|
||||
userName: '李四',
|
||||
workDate: '2026-07-01',
|
||||
timeResult: 'Normal',
|
||||
locationResult: '',
|
||||
planCheckTime: '',
|
||||
actualCheckTime: '2026-07-01T09:00:00.000Z',
|
||||
checkId: 'check-2',
|
||||
checkType: 'OnDuty',
|
||||
},
|
||||
]);
|
||||
|
||||
const events: Array<{ phase: string; userId?: number }> = [];
|
||||
const sub = service.progress$.subscribe((event) => {
|
||||
events.push({ phase: event.phase, userId: event.userId });
|
||||
});
|
||||
|
||||
await service.importFromDingTalk({
|
||||
startDate: '2026-07-01',
|
||||
endDate: '2026-07-01',
|
||||
userIds: ['ding-1'],
|
||||
userId: 42,
|
||||
});
|
||||
|
||||
sub.unsubscribe();
|
||||
|
||||
expect(events.length).toBeGreaterThan(0);
|
||||
for (const event of events) {
|
||||
expect(event.userId).toBe(42);
|
||||
}
|
||||
});
|
||||
|
||||
it('emits userId undefined when import has no HTTP user', async () => {
|
||||
dingTalkService.fetchAttendanceResults.mockResolvedValue([
|
||||
{
|
||||
userId: 'ding-2',
|
||||
userName: '王五',
|
||||
workDate: '2026-07-02',
|
||||
timeResult: 'Normal',
|
||||
locationResult: '',
|
||||
planCheckTime: '',
|
||||
actualCheckTime: '2026-07-02T10:00:00.000Z',
|
||||
checkId: 'check-3',
|
||||
checkType: 'OnDuty',
|
||||
},
|
||||
]);
|
||||
|
||||
const events: Array<{ phase: string; userId?: number }> = [];
|
||||
const sub = service.progress$.subscribe((event) => {
|
||||
events.push({ phase: event.phase, userId: event.userId });
|
||||
});
|
||||
|
||||
await service.importFromDingTalk({
|
||||
startDate: '2026-07-02',
|
||||
endDate: '2026-07-02',
|
||||
userIds: ['ding-2'],
|
||||
});
|
||||
|
||||
sub.unsubscribe();
|
||||
|
||||
expect(events.length).toBeGreaterThan(0);
|
||||
for (const event of events) {
|
||||
expect(event.userId).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user