fix(abtest): clarify admin token authentication errors

This commit is contained in:
COMPUTER\EDY 2026-09-10 18:32:51 +08:00
parent d477dd54c0
commit f7335a5262
3 changed files with 158 additions and 4 deletions

View File

@ -1,4 +1,4 @@
import test from "node:test"; import test, { afterEach, beforeEach } from "node:test";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { readFile } from "node:fs/promises"; import { readFile } from "node:fs/promises";
import { registerHooks } from "node:module"; import { registerHooks } from "node:module";
@ -7,6 +7,7 @@ const functionUrl = new URL("../functions/abConfigAdmin.ts", import.meta.url);
const state = { const state = {
configs: [], configs: [],
nextId: 1, nextId: 1,
collectionCalls: 0,
}; };
function clone(value) { function clone(value) {
@ -19,6 +20,7 @@ function matches(document, query) {
function collection(name) { function collection(name) {
assert.equal(name, "ab_config"); assert.equal(name, "ab_config");
state.collectionCalls++;
return { return {
async get() { async get() {
return { data: clone(state.configs) }; return { data: clone(state.configs) };
@ -64,13 +66,29 @@ registerHooks({
} }
return nextResolve(specifier, context); 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); 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() { function reset() {
state.configs = []; state.configs = [];
state.nextId = 1; state.nextId = 1;
state.collectionCalls = 0;
} }
function config(extra = {}) { function config(extra = {}) {
@ -95,6 +113,54 @@ async function save(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 () => { test("resetVersion changes only on an enabled false-to-true transition", async () => {
reset(); reset();

View File

@ -0,0 +1,79 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { runInNewContext } from "node:vm";
const html = await readFile(new URL("../../xxwz.html", import.meta.url), "utf8");
const script = html.match(/<script>([\s\S]*?)<\/script>/)[1];
function loadPage(responseText) {
const elements = new Map();
const stored = new Map();
const requests = [];
function element() {
return { value: "", style: {}, appendChild() {}, querySelectorAll() { return []; } };
}
const document = {
getElementById(id) {
if (!elements.has(id)) elements.set(id, element());
return elements.get(id);
},
createElement: element,
};
runInNewContext(script, {
document,
localStorage: { getItem: key => stored.get(key), setItem: (key, value) => stored.set(key, value) },
async fetch(url, options) {
requests.push({ url, ...options });
return { text: async () => responseText };
},
});
return { elements, requests };
}
test("an empty admin token is rejected before sending a request", async () => {
const { elements, requests } = loadPage("");
await elements.get("readBtn").onclick();
assert.equal(requests.length, 0);
assert.equal(elements.get("status").textContent, "请填写 Admin Token");
});
test("unauthorized explains the environment token and preserves the current config", async () => {
const { elements, requests } = loadPage(JSON.stringify({ code: 0, data: null, msg: "unauthorized" }));
elements.get("adminToken").value = "abtest-admin-token";
const before = elements.get("configPreview").textContent;
await elements.get("readBtn").onclick();
assert.equal(requests.length, 1);
assert.match(elements.get("status").textContent, /unauthorized.*AB_ADMIN_TOKEN/);
assert.match(elements.get("status").textContent, /仅在服务器未设置/);
assert.equal(elements.get("configPreview").textContent, before);
});
test("reading config sends the entered token unchanged in a JSON POST", async () => {
const { elements, requests } = loadPage(JSON.stringify({ code: 1, data: [], msg: "ok" }));
elements.get("apiUrl").value = " https://example.com/abConfigAdmin ";
elements.get("adminToken").value = "configured-token";
await elements.get("readBtn").onclick();
assert.equal(requests.length, 1);
assert.equal(requests[0].url, "https://example.com/abConfigAdmin");
assert.equal(requests[0].method, "POST");
assert.equal(requests[0].headers["Content-Type"], "application/json");
assert.deepEqual(JSON.parse(requests[0].body), { action: "list", adminToken: "configured-token" });
assert.equal(elements.get("status").textContent, "已读取线上配置并覆盖当前页面");
assert.equal(elements.get("configPreview").textContent, "[]");
});
test("a non-JSON response still reports an invalid cloud function URL", async () => {
const { elements } = loadPage("<html>Not found</html>");
elements.get("adminToken").value = "configured-token";
await elements.get("readBtn").onclick();
assert.equal(elements.get("status").textContent, "云函数地址返回的不是 JSON,请检查 URL");
});

View File

@ -419,7 +419,8 @@
<input id="apiUrl" placeholder="https://q6rvwvtnga.sealoshzh.site/abConfigAdmin/"> <input id="apiUrl" placeholder="https://q6rvwvtnga.sealoshzh.site/abConfigAdmin/">
<label for="adminToken">Admin Token</label> <label for="adminToken">Admin Token</label>
<input id="adminToken" type="password" placeholder="测试默认: abtest-admin-token"> <input id="adminToken" type="password" placeholder="请输入服务器配置的 Admin Token">
<div class="reset-help">须与目标云函数的 AB_ADMIN_TOKEN 环境变量一致;仅未设置时使用默认值 abtest-admin-token。</div>
<div class="status" id="status"></div> <div class="status" id="status"></div>
</section> </section>
@ -527,22 +528,30 @@
} }
async function callAdmin(payload) { async function callAdmin(payload) {
remember();
const url = apiUrl.value.trim(); const url = apiUrl.value.trim();
if (!url) { if (!url) {
throw new Error("请填写云函数 URL"); throw new Error("请填写云函数 URL");
} }
if (!adminToken.value) {
throw new Error("请填写 Admin Token");
}
remember();
const res = await fetch(url, { const res = await fetch(url, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ adminToken: adminToken.value, ...payload }) body: JSON.stringify({ adminToken: adminToken.value, ...payload })
}); });
const text = await res.text(); const text = await res.text();
let ret;
try { try {
return JSON.parse(text); ret = JSON.parse(text);
} catch { } catch {
throw new Error("云函数地址返回的不是 JSON,请检查 URL"); throw new Error("云函数地址返回的不是 JSON,请检查 URL");
} }
if (ret.msg === "unauthorized") {
throw new Error("鉴权失败(unauthorized):请确认云函数 URL 指向正确环境,Admin Token 与该环境的 AB_ADMIN_TOKEN 一致,且未多输入空格。abtest-admin-token 仅在服务器未设置 AB_ADMIN_TOKEN 时有效。");
}
return ret;
} }
async function saveConfig() { async function saveConfig() {