fix(abtest): clarify admin token authentication errors
This commit is contained in:
parent
d477dd54c0
commit
f7335a5262
|
|
@ -1,4 +1,4 @@
|
|||
import test from "node:test";
|
||||
import test, { afterEach, beforeEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { registerHooks } from "node:module";
|
||||
|
|
@ -7,6 +7,7 @@ const functionUrl = new URL("../functions/abConfigAdmin.ts", import.meta.url);
|
|||
const state = {
|
||||
configs: [],
|
||||
nextId: 1,
|
||||
collectionCalls: 0,
|
||||
};
|
||||
|
||||
function clone(value) {
|
||||
|
|
@ -19,6 +20,7 @@ function matches(document, query) {
|
|||
|
||||
function collection(name) {
|
||||
assert.equal(name, "ab_config");
|
||||
state.collectionCalls++;
|
||||
return {
|
||||
async get() {
|
||||
return { data: clone(state.configs) };
|
||||
|
|
@ -64,13 +66,29 @@ registerHooks({
|
|||
}
|
||||
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 = {}) {
|
||||
|
|
@ -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 () => {
|
||||
reset();
|
||||
|
||||
|
|
|
|||
79
server/laf-cloud/tests/ab-config-ui.test.mjs
Normal file
79
server/laf-cloud/tests/ab-config-ui.test.mjs
Normal 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");
|
||||
});
|
||||
|
|
@ -419,7 +419,8 @@
|
|||
<input id="apiUrl" placeholder="https://q6rvwvtnga.sealoshzh.site/abConfigAdmin/">
|
||||
|
||||
<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>
|
||||
</section>
|
||||
|
|
@ -527,22 +528,30 @@
|
|||
}
|
||||
|
||||
async function callAdmin(payload) {
|
||||
remember();
|
||||
const url = apiUrl.value.trim();
|
||||
if (!url) {
|
||||
throw new Error("请填写云函数 URL");
|
||||
}
|
||||
if (!adminToken.value) {
|
||||
throw new Error("请填写 Admin Token");
|
||||
}
|
||||
remember();
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ adminToken: adminToken.value, ...payload })
|
||||
});
|
||||
const text = await res.text();
|
||||
let ret;
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
ret = JSON.parse(text);
|
||||
} catch {
|
||||
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() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user