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 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();
|
||||||
|
|
||||||
|
|
|
||||||
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/">
|
<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() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user