import test, { afterEach, beforeEach } from "node:test"; import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import { registerHooks } from "node:module"; const functionUrl = new URL("../functions/abConfigAdmin.ts", import.meta.url); const state = { configs: [], nextId: 1, collectionCalls: 0, }; function clone(value) { return value == null ? value : JSON.parse(JSON.stringify(value)); } function matches(document, query) { return Object.keys(query).every(key => document[key] === query[key]); } function collection(name) { assert.equal(name, "ab_config"); state.collectionCalls++; return { async get() { return { data: clone(state.configs) }; }, where(query) { return { async getOne() { return { data: clone(state.configs.find(item => matches(item, query)) || null) }; }, async update(payload) { const matched = state.configs.filter(item => matches(item, query)); matched.forEach(item => Object.assign(item, clone(payload))); return { updated: matched.length }; }, async remove() { const oldLength = state.configs.length; state.configs = state.configs.filter(item => !matches(item, query)); return { deleted: oldLength - state.configs.length }; }, }; }, async add(payload) { const document = { _id: `config-${state.nextId++}`, ...clone(payload) }; state.configs.push(document); return { id: document._id }; }, }; } globalThis.__abConfigAdminCloudMock = { database() { return { collection }; }, }; registerHooks({ resolve(specifier, context, nextResolve) { if (specifier === "@lafjs/cloud") { return { url: "data:text/javascript,export default globalThis.__abConfigAdminCloudMock", shortCircuit: true, }; } return nextResolve(specifier, context); }, load(url, context, nextLoad) { return nextLoad(url, url === functionUrl.href ? { ...context, format: "module-typescript" } : context); }, }); const { default: abConfigAdmin } = await import(functionUrl); let previousAdminToken; beforeEach(() => { previousAdminToken = process.env.AB_ADMIN_TOKEN; delete process.env.AB_ADMIN_TOKEN; }); afterEach(() => { if (previousAdminToken === undefined) delete process.env.AB_ADMIN_TOKEN; else process.env.AB_ADMIN_TOKEN = previousAdminToken; }); function reset() { state.configs = []; state.nextId = 1; state.collectionCalls = 0; } function config(extra = {}) { return { experimentId: "exp_layer_1_001", layer: "1", name: "Layer 1", enabled: true, resetAssignments: false, variants: [{ group: "A", ratio: 50 }, { group: "B", ratio: 50 }], ...extra, }; } async function save(items) { return await abConfigAdmin({ body: { action: "save", adminToken: "abtest-admin-token", config: items, }, }); } test("the default admin token lists configs from body or query when no override is set", async () => { reset(); state.configs.push(config()); for (const source of ["body", "query"]) { const result = await abConfigAdmin({ [source]: { action: "list", adminToken: "abtest-admin-token" }, }); assert.deepEqual(result, { code: 1, data: state.configs, msg: "ok" }); } assert.equal(state.collectionCalls, 2); }); test("AB_ADMIN_TOKEN overrides the default token for body and query requests", async () => { reset(); process.env.AB_ADMIN_TOKEN = "custom-test-admin-token"; for (const source of ["body", "query"]) { const rejected = await abConfigAdmin({ [source]: { action: "list", adminToken: "abtest-admin-token" }, }); assert.deepEqual(rejected, { code: 0, data: null, msg: "unauthorized" }); } assert.equal(state.collectionCalls, 0); for (const source of ["body", "query"]) { const accepted = await abConfigAdmin({ [source]: { action: "list", adminToken: "custom-test-admin-token" }, }); assert.deepEqual(accepted, { code: 1, data: [], msg: "ok" }); } assert.equal(state.collectionCalls, 2); }); test("missing or incorrect tokens reject reads and writes before accessing the database", async () => { reset(); for (const adminToken of [undefined, "incorrect-admin-token"]) { for (const action of ["list", "save", "disable", "enable", "delete"]) { const result = await abConfigAdmin({ body: { action, adminToken, config: config(), experimentId: "exp_layer_1_001" }, }); assert.deepEqual(result, { code: 0, data: null, msg: "unauthorized" }); } } assert.equal(state.collectionCalls, 0); }); test("resetVersion changes only on an enabled false-to-true transition", async () => { reset(); await save([config()]); assert.equal(state.configs[0].resetVersion, 0); await save([config({ resetAssignments: true })]); const firstVersion = state.configs[0].resetVersion; assert.ok(firstVersion > 0); await save([config({ experimentId: "exp_layer_1_002", resetAssignments: true, variants: [{ group: "A", ratio: 80 }, { group: "B", ratio: 20 }], })]); assert.equal(state.configs.length, 1); assert.equal(state.configs[0].experimentId, "exp_layer_1_002"); assert.equal(state.configs[0].resetVersion, firstVersion); await save([config({ experimentId: "exp_layer_1_002", resetAssignments: false })]); assert.equal(state.configs[0].resetVersion, firstVersion); await save([config({ experimentId: "exp_layer_1_002", resetAssignments: true })]); assert.ok(state.configs[0].resetVersion > firstVersion); }); test("a disabled layer does not create a reset version", async () => { reset(); await save([config({ enabled: false })]); await save([config({ enabled: false, resetAssignments: true })]); assert.equal(state.configs[0].resetVersion, 0); await save([config({ enabled: true, resetAssignments: true })]); assert.equal(state.configs[0].resetVersion, 0); await save([config({ enabled: true, resetAssignments: false })]); await save([config({ enabled: true, resetAssignments: true })]); assert.ok(state.configs[0].resetVersion > 0); await save([config({ enabled: false, resetAssignments: true })]); assert.equal(state.configs[0].resetVersion, 0); await save([config({ enabled: true, resetAssignments: true })]); assert.equal(state.configs[0].resetVersion, 0); }); test("an existing enabled reset without a version is initialized on publish", async () => { reset(); state.configs.push({ _id: "legacy", ...config({ resetAssignments: true }) }); const result = await save([config({ resetAssignments: true })]); assert.equal(result.code, 1); assert.ok(state.configs[0].resetVersion > 0); }); test("Default is a case-insensitive reserved group name", async () => { reset(); const result = await save([config({ variants: [{ group: "DEFAULT", ratio: 50 }, { group: "B", ratio: 50 }], })]); assert.equal(result.code, 0); assert.match(result.msg, /Default.*reserved/i); assert.equal(state.configs.length, 0); }); test("the admin page explains edge-triggered reset behavior", async () => { const ui = await readFile(new URL("../../xxwz.html", import.meta.url), "utf8"); assert.match(ui, /关闭→开启并发布/); assert.match(ui, /关闭后未登录用户不补做/); assert.match(ui, /Default.*系统保留/); });