MatchMaster/server/laf-cloud/tests/ab-config-admin.test.mjs

240 lines
7.2 KiB
JavaScript

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.*系统保留/);
});