ci: 统一测试入口 scripts/ci.mjs(协议/用户管理/单测/双设备/实时链路/构建/依赖审计)+ Gitea Actions 工作流
This commit is contained in:
@@ -1,3 +1,8 @@
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
electron: true
|
||||
|
||||
# 允许执行安装脚本的依赖(esbuild 需要准备平台二进制)
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
- electron
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
// 纯函数单元测试:日期库 / 考勤计算 / 加解密(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))
|
||||
})
|
||||
@@ -2,7 +2,7 @@ import type { Attendance } from '../db/schema'
|
||||
import type { WorkRule } from '../db/repo'
|
||||
import { hhmm, isWeekend, tsMinutes } from '../lib/date'
|
||||
|
||||
export type DayStatus = 'normal' | 'late' | 'early' | 'incomplete' | 'absent' | 'none' | 'rest'
|
||||
export type DayStatus = 'normal' | 'late' | 'early' | 'incomplete' | 'absent' | 'none' | 'rest' | 'leave'
|
||||
|
||||
export interface DayAttendanceInfo {
|
||||
status: DayStatus
|
||||
|
||||
Reference in New Issue
Block a user