246 行
8.1 KiB
JavaScript
246 行
8.1 KiB
JavaScript
const {
|
|
buildSecretCheck,
|
|
buildWriteGate,
|
|
executeCheckinSummaryArchive,
|
|
parseArgs,
|
|
} = require("../../../src/scripts/check-wecom-checkin-summary-archive");
|
|
const {
|
|
sanitizeCheckinApiResponse,
|
|
} = require("../../../src/services/integrations/wecomCheckin");
|
|
|
|
const SENSITIVE_USER_ID = ["checkin", "summary", "full", "userid", "should", "not", "leak"].join("_");
|
|
const SENSITIVE_AGENT_SECRET = ["checkin", "agent", "secret", "should", "not", "leak"].join("-");
|
|
const SENSITIVE_CHECKIN_SECRET = ["checkin", "override", "secret", "should", "not", "leak"].join("-");
|
|
|
|
function baseConfig(overrides = {}) {
|
|
return {
|
|
appTimezone: "Asia/Shanghai",
|
|
wecomCorpId: "corp",
|
|
wecomAgentId: "1000002",
|
|
wecomAgentSecret: SENSITIVE_AGENT_SECRET,
|
|
wecomCheckinSecret: "",
|
|
wecomRequestTimeoutMs: 1000,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function employeeRows(overrides = {}) {
|
|
return [
|
|
{
|
|
id: "employee-1",
|
|
employee_no: "SMOKE002",
|
|
status: "active",
|
|
archived_at: null,
|
|
wecom_identity_binding: { external_user_id: SENSITIVE_USER_ID },
|
|
},
|
|
{
|
|
id: "employee-2",
|
|
employee_no: "SMOKE101",
|
|
status: "active",
|
|
archived_at: null,
|
|
wecom_identity_binding: { external_user_id: `${SENSITIVE_USER_ID}_approver` },
|
|
},
|
|
].map((row) => ({ ...row, ...(overrides[row.employee_no] || {}) }));
|
|
}
|
|
|
|
function prismaStub() {
|
|
const dailyRows = [
|
|
{
|
|
employee_id: "employee-1",
|
|
external_user_id_prefix: "chec***",
|
|
checkin_date: new Date("2026-05-20T00:00:00.000Z"),
|
|
record_count: 1,
|
|
first_checkin_at: new Date("2026-05-20T01:00:00.000Z"),
|
|
last_checkin_at: new Date("2026-05-20T01:00:00.000Z"),
|
|
exception_count: 0,
|
|
abnormal_count: 0,
|
|
result_summary: { normal: 1 },
|
|
raw_hash_digest: "a".repeat(64),
|
|
},
|
|
];
|
|
return {
|
|
employee: { findMany: vi.fn(async () => employeeRows()) },
|
|
wecomCheckinSyncRun: {
|
|
create: vi.fn(async () => ({ id: "sync-run-1" })),
|
|
update: vi.fn(async () => ({ id: "sync-run-1" })),
|
|
},
|
|
wecomCheckinRecord: {
|
|
createMany: vi.fn(async () => ({ count: 1 })),
|
|
findMany: vi.fn(async () => [
|
|
{
|
|
sync_run_id: "sync-run-1",
|
|
employee_id: "employee-1",
|
|
external_user_id: SENSITIVE_USER_ID,
|
|
checkin_time: new Date("2026-05-20T01:00:00.000Z"),
|
|
checkin_date: new Date("2026-05-20T00:00:00.000Z"),
|
|
checkin_type: "OnDuty",
|
|
exception_type: null,
|
|
source_type: "app",
|
|
result: "normal",
|
|
raw_hash: "a".repeat(64),
|
|
},
|
|
]),
|
|
},
|
|
wecomCheckinDailySummary: {
|
|
upsert: vi.fn(async () => ({})),
|
|
findMany: vi.fn(async () => dailyRows),
|
|
},
|
|
wecomCheckinMonthlySummary: {
|
|
upsert: vi.fn(async () => ({})),
|
|
findMany: vi.fn(async () => [
|
|
{
|
|
employee_id: "employee-1",
|
|
month: "2026-05",
|
|
summary_day_count: 1,
|
|
checkin_day_count: 1,
|
|
exception_day_count: 0,
|
|
abnormal_record_count: 0,
|
|
missing_day_candidate_count: 0,
|
|
first_checkin_at: new Date("2026-05-20T01:00:00.000Z"),
|
|
last_checkin_at: new Date("2026-05-20T01:00:00.000Z"),
|
|
},
|
|
]),
|
|
},
|
|
wecomCheckinRuleSnapshot: {
|
|
upsert: vi.fn(async () => ({})),
|
|
findMany: vi.fn(async () => [
|
|
{
|
|
employee_id: "employee-1",
|
|
external_user_id_prefix: "chec***",
|
|
snapshot_date: new Date("2026-05-20T00:00:00.000Z"),
|
|
rule_hash: "b".repeat(64),
|
|
rule_summary: { keyCount: 2 },
|
|
},
|
|
]),
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("check-wecom-checkin-summary-archive", () => {
|
|
it("parses defaults and write gate", () => {
|
|
const options = parseArgs(["--live", "--persist-to-db", "--employee-no=SMOKE002", "--format=json"]);
|
|
expect(options.live).toBe(true);
|
|
expect(options.persistToDb).toBe(true);
|
|
expect(options.employeeNos).toEqual(["SMOKE002"]);
|
|
expect(buildWriteGate(options, {})).toMatchObject({
|
|
status: "NEEDS_CONFIG",
|
|
missing: ["WECOM_CHECKIN_ARCHIVE_WRITE"],
|
|
});
|
|
expect(buildWriteGate(options, { WECOM_CHECKIN_ARCHIVE_WRITE: "1" })).toMatchObject({
|
|
status: "OK",
|
|
enabled: true,
|
|
});
|
|
});
|
|
|
|
it("documents fixed Secret source with explicit checkin override", () => {
|
|
expect(buildSecretCheck(baseConfig())).toMatchObject({
|
|
source: "WECOM_AGENT_SECRET",
|
|
configured: true,
|
|
});
|
|
expect(buildSecretCheck(baseConfig({ wecomCheckinSecret: SENSITIVE_CHECKIN_SECRET }))).toMatchObject({
|
|
source: "WECOM_CHECKIN_SECRET",
|
|
configured: true,
|
|
override: true,
|
|
});
|
|
});
|
|
|
|
it("reads offline archive without WeCom API and redacts report", async () => {
|
|
const prisma = prismaStub();
|
|
const report = await executeCheckinSummaryArchive(parseArgs(["--offline", "--format=json"]), {
|
|
prisma,
|
|
config: baseConfig(),
|
|
employees: employeeRows(),
|
|
generatedAt: "2026-05-20T04:00:00.000Z",
|
|
});
|
|
|
|
expect(report.status).toBe("PASS_CHECKIN_ARCHIVE_OFFLINE_READY");
|
|
expect(report.checks.offline.counts.daily).toBe(1);
|
|
expect(JSON.stringify(report)).not.toContain(SENSITIVE_USER_ID);
|
|
});
|
|
|
|
it("live persists OA summaries only after write env", async () => {
|
|
const prisma = prismaStub();
|
|
const fetchCheckinDataImpl = vi.fn(async ({ requireSyncEnabled, externalUserIds }) => {
|
|
expect(requireSyncEnabled).toBe(false);
|
|
expect(externalUserIds).toContain(SENSITIVE_USER_ID);
|
|
return {
|
|
ok: true,
|
|
code: "wecom_checkin_fetched",
|
|
records: [
|
|
{
|
|
userid: SENSITIVE_USER_ID,
|
|
checkin_time: 1779238800,
|
|
checkin_type: "OnDuty",
|
|
result: "normal",
|
|
},
|
|
],
|
|
response: { errcode: 0, checkindataCount: 1 },
|
|
};
|
|
});
|
|
const fetchCheckinRulesImpl = vi.fn(async () => ({
|
|
ok: true,
|
|
code: "wecom_checkin_rule_fetched",
|
|
rules: [{ userid: SENSITIVE_USER_ID, groupname: "默认", loc_info: "must-not-leak" }],
|
|
response: { errcode: 0, ruleInfoCount: 1 },
|
|
}));
|
|
|
|
const report = await executeCheckinSummaryArchive(parseArgs([
|
|
"--live",
|
|
"--persist-to-db",
|
|
"--format=json",
|
|
"--start-date=2026-05-20",
|
|
"--end-date=2026-05-20",
|
|
]), {
|
|
prisma,
|
|
config: baseConfig(),
|
|
employees: employeeRows(),
|
|
env: { WECOM_CHECKIN_ARCHIVE_WRITE: "1" },
|
|
fetchCheckinDataImpl,
|
|
fetchCheckinRulesImpl,
|
|
generatedAt: "2026-05-20T04:00:00.000Z",
|
|
});
|
|
|
|
expect(report.status).toBe("PASS_CHECKIN_ARCHIVE_READY");
|
|
expect(prisma.wecomCheckinRecord.createMany).toHaveBeenCalledTimes(1);
|
|
expect(prisma.wecomCheckinDailySummary.upsert).toHaveBeenCalled();
|
|
expect(prisma.wecomCheckinRuleSnapshot.upsert).toHaveBeenCalled();
|
|
const text = JSON.stringify(report);
|
|
expect(text).not.toContain(SENSITIVE_USER_ID);
|
|
expect(text).not.toContain(SENSITIVE_AGENT_SECRET);
|
|
expect(text).not.toContain("must-not-leak");
|
|
});
|
|
|
|
it("classifies API errors as external config blockers", async () => {
|
|
const shortSensitiveUserId = "SMOKE002";
|
|
const report = await executeCheckinSummaryArchive(parseArgs(["--live", "--format=json"]), {
|
|
prisma: prismaStub(),
|
|
config: baseConfig(),
|
|
employees: employeeRows(),
|
|
fetchCheckinDataImpl: vi.fn(async () => ({
|
|
ok: false,
|
|
code: "wecom_checkin_api_error",
|
|
message: "WeCom checkin errcode 301062",
|
|
records: [],
|
|
response: { errcode: 301062, invaliduser: "SM***" },
|
|
})),
|
|
fetchCheckinRulesImpl: vi.fn(),
|
|
});
|
|
|
|
expect(report.status).toBe("SKIP");
|
|
expect(report.conclusion).toBe("BLOCKED_BY_EXTERNAL_CONFIG");
|
|
expect(JSON.stringify(report.checks.live.api.response)).not.toContain(shortSensitiveUserId);
|
|
});
|
|
|
|
it("redacts invaliduser values from checkin API responses", () => {
|
|
const sanitized = sanitizeCheckinApiResponse({
|
|
errcode: 301062,
|
|
errmsg: "invalid user",
|
|
invaliduser: "SMOKE002|SMOKE101",
|
|
});
|
|
|
|
expect(sanitized.invaliduser).toBe("SM***|SM***");
|
|
expect(JSON.stringify(sanitized)).not.toContain("SMOKE002");
|
|
expect(JSON.stringify(sanitized)).not.toContain("SMOKE101");
|
|
});
|
|
});
|