/** * 操作记录(审计)单元测试 * 覆盖:appendAuditLog/loadAuditLogs 读写与过滤、auditService.log 静默容错、getClientIp 支持 nginx 转发 * 运行:npm test */ const test = require("node:test"); const assert = require("node:assert"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); // 使用临时数据目录,避免污染真实 data/ const tmpDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "registry-audit-test-")); process.env.REGISTRY_DATA_DIR = tmpDataDir; const duckdbAdapter = require("../src/persistence/duckdbAdapter"); const { log: auditLog, getClientIp } = require("../src/services/auditService"); test.before(async () => { await duckdbAdapter.init(); }); test("记录一条配置操作并可按项目/键查询", async () => { await auditLog({ source: "web", actor: "admin", ip: "203.0.113.9", projectId: "proj-a", key: "db/host", action: "update", value: "db.internal", detail: { from: { key: "db/host", value: "old" }, to: { key: "db/host", value: "db.internal" } }, }); const byProject = await duckdbAdapter.loadAuditLogs({ projectId: "proj-a" }); assert.strictEqual(byProject.items.length, 1); assert.strictEqual(byProject.total, 1); const byKey = await duckdbAdapter.loadAuditLogs({ projectId: "proj-a", key: "db/host" }); assert.strictEqual(byKey.items.length, 1); // 其它项目/键查不到 assert.strictEqual((await duckdbAdapter.loadAuditLogs({ projectId: "proj-b" })).items.length, 0); assert.strictEqual((await duckdbAdapter.loadAuditLogs({ projectId: "proj-a", key: "other" })).items.length, 0); const rec = byKey.items[0]; assert.strictEqual(rec.source, "web"); assert.strictEqual(rec.actor, "admin"); assert.strictEqual(rec.ip, "203.0.113.9"); assert.strictEqual(rec.action, "update"); assert.strictEqual(rec.value, "db.internal"); assert.strictEqual(rec.detail.from.value, "old"); assert.ok(rec.createdAt); }); test("记录按时间倒序返回,MCP 来源也正确记录 Token 名", async () => { await auditLog({ source: "mcp", actor: "ci-token", ip: "10.0.0.1", projectId: "proj-a", key: "k/1", action: "read", value: "x" }); await auditLog({ source: "mcp", actor: "ci-token", ip: "10.0.0.1", projectId: "proj-a", key: "k/1", action: "create", value: "x" }); const logs = await duckdbAdapter.loadAuditLogs({ projectId: "proj-a", key: "k/1" }); const items = logs.items; assert.strictEqual(items.length, 2); assert.ok(items[0].createdAt >= items[1].createdAt, "最新的应在前"); assert.ok(items.every((l) => l.source === "mcp" && l.actor === "ci-token")); }); test("分页与过滤:action / start-end 时间范围 / 键名模糊匹配 / 页码切片", async () => { const base = Date.now() - 5 * 60000; for (let i = 1; i <= 3; i++) { await duckdbAdapter.appendAuditLog({ id: "aud-pg-" + i, source: "mcp", actor: "ci-token", projectId: "proj-p", key: `svc/app-${i}`, action: i === 3 ? "delete" : "create", value: `v${i}`, createdAt: new Date(base + i * 1000).toISOString(), }); } // 分页:page=1 取 2 条,total 应为 3 const page1 = await duckdbAdapter.loadAuditLogs({ projectId: "proj-p", page: 1, pageSize: 2 }); assert.strictEqual(page1.items.length, 2); assert.strictEqual(page1.total, 3); assert.strictEqual(page1.page, 1); assert.strictEqual(page1.pageSize, 2); const page2 = await duckdbAdapter.loadAuditLogs({ projectId: "proj-p", page: 2, pageSize: 2 }); assert.strictEqual(page2.items.length, 1); // action 过滤 const creates = await duckdbAdapter.loadAuditLogs({ projectId: "proj-p", action: "create" }); assert.strictEqual(creates.items.length, 2); assert.strictEqual(creates.items.every((l) => l.action === "create"), true); const deletes = await duckdbAdapter.loadAuditLogs({ projectId: "proj-p", action: "delete" }); assert.strictEqual(deletes.items.length, 1); // key 模糊匹配 const fuzzy = await duckdbAdapter.loadAuditLogs({ projectId: "proj-p", key: "app-" }); assert.strictEqual(fuzzy.items.length, 3); // 时间范围:取中间那条 const midStart = new Date(base + 1500).toISOString(); const midEnd = new Date(base + 2500).toISOString(); const range = await duckdbAdapter.loadAuditLogs({ projectId: "proj-p", start: midStart, end: midEnd }); assert.strictEqual(range.items.length, 1); assert.strictEqual(range.items[0].key, "svc/app-2"); }); test("getClientIp 优先取 X-Forwarded-For,其次 remoteAddress", () => { assert.strictEqual(getClientIp({ headers: { "x-forwarded-for": "203.0.113.50, 10.0.0.5" } }), "203.0.113.50"); assert.strictEqual(getClientIp({ headers: {}, socket: { remoteAddress: "127.0.0.1" } }), "127.0.0.1"); assert.strictEqual(getClientIp(null), ""); });