import assert from "node:assert/strict"; import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; import { configureService, renderUnit, servicePaths, startService, uninstallService } from "../src/admin.js"; test("dingtalk-admin writes remote OAuth settings and secret-free hardened unit", () => { const root = mkdtempSync(join(tmpdir(), "ding-admin-")); const old = process.env.XDG_CONFIG_HOME; process.env.XDG_CONFIG_HOME = join(root, "config"); try { mkdirSync(join(root, ".secrets"), { mode: 0o700 }); writeFileSync(join(root, ".secrets", "dingtalk.json"), "{}", { mode: 0o600 }); writeFileSync(join(root, ".secrets", "xft.key"), "secret", { mode: 0o600 }); chmodSync(join(root, ".secrets", "dingtalk.json"), 0o600); const paths = configureService({ projectRoot: root, publicBaseUrl: "https://approval.example.com", corpId: "ding-corp", port: 3000, credentialsFile: ".secrets/dingtalk.json", xftOpenApiAppId: "app", xftOpenApiSecretFile: ".secrets/xft.key", xftOpenApiEnterpriseId: "enterprise", dingtalkClientOnlyProcessCodes: "PROC-SIGN,PROC-CLIENT", }); assert.equal(statSync(paths.env).mode & 0o777, 0o600); assert.equal(statSync(paths.refSecret).mode & 0o777, 0o600); assert.equal(statSync(paths.sessionSecret).mode & 0o777, 0o600); const runtime = readFileSync(paths.env, "utf8"); assert.match(runtime, /MCP_REMOTE_AUTH_ENABLED="true"/); assert.match(runtime, /DINGTALK_CORP_ID="ding-corp"/); assert.match(runtime, /MCP_AUTH_DATABASE=/); assert.match(runtime, /APP_MODE="gateway"/); assert.match(runtime, /DINGTALK_CLIENT_ONLY_PROCESS_CODES="PROC-SIGN,PROC-CLIENT"/); assert.doesNotMatch(runtime, /DWS_/); assert.doesNotMatch(runtime, /MCP_TRUSTED_LOCAL_USER_ID|CLIENT_SECRET/); const unit = renderUnit(paths, "/usr/bin/node"); assert.match(unit, /ProtectSystem=strict/); assert.doesNotMatch(unit, /ding-corp|CLIENT_SECRET/); mkdirSync(join(root, "dist", "src"), { recursive: true }); writeFileSync(join(root, "dist", "src", "index.js"), "// built"); const calls: string[][] = []; startService(root, (args) => calls.push(args)); assert.deepEqual(calls.map((call) => call[0]), ["daemon-reload", "enable", "restart"]); uninstallService(root, (args) => calls.push(args)); assert.equal(existsSync(paths.env), false); assert.equal(existsSync(paths.refSecret), true); assert.equal(existsSync(paths.sessionSecret), true); } finally { if (old === undefined) delete process.env.XDG_CONFIG_HOME; else process.env.XDG_CONFIG_HOME = old; } }); test("dingtalk-admin requires HTTPS and references XFT secret files", () => { const root = mkdtempSync(join(tmpdir(), "ding-admin-openapi-")); mkdirSync(join(root, ".secrets"), { mode: 0o700 }); writeFileSync(join(root, ".secrets", "dingtalk.json"), "{}", { mode: 0o600 }); writeFileSync(join(root, ".secrets", "xft.key"), "1".repeat(32), { mode: 0o600 }); assert.throws(() => configureService({ projectRoot: root, publicBaseUrl: "http://approval.example.com", corpId: "ding-corp", port: 3000, credentialsFile: ".secrets/dingtalk.json", }), /HTTPS/); const paths = configureService({ projectRoot: root, publicBaseUrl: "https://approval.example.com", corpId: "ding-corp", port: 3000, credentialsFile: ".secrets/dingtalk.json", xftOpenApiAppId: "open-app", xftOpenApiSecretFile: ".secrets/xft.key", xftOpenApiEnterpriseId: "TEST0001", }); const runtime = readFileSync(paths.env, "utf8"); assert.match(runtime, /XFT_OPEN_API_ENABLED="true"/); assert.match(runtime, /XFT_OPEN_API_AUTHORITY_SECRET_FILE=/); assert.doesNotMatch(runtime, new RegExp("1".repeat(32))); });