// 纯函数单元测试:日期库 / 考勤计算 / 加解密(node:test 运行器,esbuild 打包后执行) import test from 'node:test' import assert from 'node:assert/strict' import { addDays, addMonths, hhmm, isWeekend, nowLocalISO, parseDateStr, toDateStr, tsMinutes, weekStart, formatDateCN } from '../src/lib/date' import { computeDay } from '../src/domain/attendance' import { aesGcmDecrypt, aesGcmEncrypt, deriveKeyFromPassword, pbkdf2Hash, randomSaltB64 } from '../src/lib/crypto' const RULE = { work_start: '09:00', work_end: '18:00', grace_minutes: 10, lunch_minutes: 60, remind_weekly: 'friday' } function rec(kind: string, inAt: string | null, outAt: string | null) { return { date: inAt ? inAt.slice(0, 10) : '2025-01-02', kind, clock_in_at: inAt, clock_out_at: outAt, note: null, location: null, created_at: '2025-01-02T00:00:00' } as never } test('日期库:addDays 跨月与闰年', () => { assert.equal(addDays('2025-01-31', 1), '2025-02-01') assert.equal(addDays('2024-02-28', 1), '2024-02-29') // 闰年 assert.equal(addDays('2023-02-28', 1), '2023-03-01') // 平年 assert.equal(addDays('2025-03-01', -1), '2025-02-28') assert.equal(addDays('2024-12-31', 1), '2025-01-01') }) test('日期库:addMonths 日期截断(1-31 加一个月)', () => { assert.equal(addMonths('2025-01-31', 1), '2025-02-28') assert.equal(addMonths('2024-01-31', 1), '2024-02-29') assert.equal(addMonths('2025-01-15', 1), '2025-02-15') assert.equal(addMonths('2025-11-30', 2), '2026-01-30') }) test('日期库:weekStart 以周一为一周开始', () => { assert.equal(weekStart('2025-01-05'), '2024-12-30') // 周日 assert.equal(weekStart('2025-01-06'), '2025-01-06') // 周一 assert.equal(weekStart('2025-01-12'), '2025-01-06') // 周日 assert.equal(weekStart('2025-01-08'), '2025-01-06') // 周中 }) test('日期库:isWeekend 与格式化', () => { assert.equal(isWeekend('2025-01-04'), true) assert.equal(isWeekend('2025-01-05'), true) assert.equal(isWeekend('2025-01-06'), false) assert.equal(hhmm('2025-01-02T09:05:00'), '09:05') assert.equal(hhmm(null), '--:--') assert.match(nowLocalISO(), /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/) assert.equal(formatDateCN('2025-01-06'), '2025 年 1 月 6 日 周一') assert.equal(toDateStr(parseDateStr('2025-02-27')), '2025-02-27') assert.equal(tsMinutes('2025-01-02T09:00:00', '2025-01-02T18:00:00'), 540) }) test('考勤:正常/迟到/早退/宽限边界', () => { const day = '2025-01-02' // 周四 const normal = computeDay(rec('normal', day + 'T09:00:00', day + 'T18:00:00'), day, RULE, false) assert.equal(normal.status, 'normal') assert.equal(normal.label, '正常') assert.equal(normal.workedMinutes, 480) // 9 小时 - 1 小时午休 // 宽限边界:09:10 恰好等于上班时间+10 分钟 → 不算迟到 const edge = computeDay(rec('normal', day + 'T09:10:00', day + 'T18:00:00'), day, RULE, false) assert.equal(edge.status, 'normal') // 迟到:09:11 const late = computeDay(rec('normal', day + 'T09:11:00', day + 'T18:00:00'), day, RULE, false) assert.equal(late.status, 'late') // 早退:17:29 下班 const early = computeDay(rec('normal', day + 'T09:00:00', day + 'T17:29:00'), day, RULE, false) assert.equal(early.status, 'early') // 补卡不判迟到早退 const makeup = computeDay(rec('makeup', day + 'T10:30:00', day + 'T17:00:00'), day, RULE, false) assert.equal(makeup.status, 'normal') assert.equal(makeup.label, '补卡') assert.equal(makeup.makeup, true) }) test('考勤:进行中/未完成/缺勤/未打卡', () => { const day = '2025-01-02' const running = computeDay(rec('normal', day + 'T09:00:00', null), day, RULE, true) assert.equal(running.status, 'incomplete') assert.equal(running.label, '进行中') const unfinished = computeDay(rec('normal', day + 'T09:00:00', null), day, RULE, false) assert.equal(unfinished.status, 'incomplete') assert.equal(unfinished.label, '未完成') const absent = computeDay(undefined, day, RULE, false) assert.equal(absent.status, 'absent') const none = computeDay(undefined, day, RULE, true) assert.equal(none.status, 'none') }) test('考勤:休假与休息日', () => { const saturday = '2025-01-04' const leave = computeDay(rec('leave', '2025-01-02T09:00:00', '2025-01-02T18:00:00'), '2025-01-02', RULE, false) assert.equal(leave.status, 'leave') assert.equal(leave.label, '请假') // 休息日:即使有打卡记录也显示休息日(当前产品行为,锁定语义) const rest = computeDay(rec('normal', saturday + 'T10:00:00', saturday + 'T18:00:00'), saturday, RULE, false) assert.equal(rest.status, 'rest') assert.equal(rest.label, '休息日') // 休息日无记录 const restEmpty = computeDay(undefined, saturday, RULE, false) assert.equal(restEmpty.status, 'rest') }) test('考勤:异常时长不出现负数', () => { const bad = computeDay(rec('normal', '2025-01-02T18:00:00', '2025-01-02T09:00:00'), '2025-01-02', RULE, false) assert.equal(bad.workedMinutes, 0) }) test('加密:哈希确定性、加盐、AES-GCM 往返', async () => { // 盐必须是合法 base64(与 randomSaltB64 的产出格式一致) const saltA = Buffer.from('salt-a').toString('base64') const saltB = Buffer.from('salt-b').toString('base64') const h1 = await pbkdf2Hash('密码abc123', saltA) const h2 = await pbkdf2Hash('密码abc123', saltA) const h3 = await pbkdf2Hash('密码abc123', saltB) const h4 = await pbkdf2Hash('密码abc124', saltA) assert.equal(h1, h2) assert.notEqual(h1, h3) assert.notEqual(h1, h4) assert.equal(Buffer.from(h1, 'base64').length, 32) const salt = randomSaltB64() assert.equal(Buffer.from(salt, 'base64').length, 16) const key = await deriveKeyFromPassword('主密码!@#', salt) const enc = await aesGcmEncrypt(key, '机密内容:服务器管理员密码') assert.notEqual(enc, '机密内容:服务器管理员密码') assert.equal(await aesGcmDecrypt(key, enc), '机密内容:服务器管理员密码') const otherKey = await deriveKeyFromPassword('错误主密码', salt) await assert.rejects(() => aesGcmDecrypt(otherKey, enc)) })