Scert/frontend/src/views/Dashboard/DomainForm.test.js
xuans 7167d0de78 修复托管域名配置误关闭和泛域名校验提示
移除域名配置弹窗背景点击关闭,避免鼠标选择输入内容时误关闭;补充泛域名格式提示和前端校验,新增回归测试。
2026-10-16 01:51:15 +08:00

294 lines
12 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import { ref } from "vue";
import { useDomainForm } from "./DomainForm.js";
import { deferred, mountComposable } from "../../test-support/mount.js";
const firstServer = { id: "server-one", project_id: "default", name: "共享服务器一" };
const secondServer = { id: "server-two", project_id: "default", name: "共享服务器二" };
const otherServer = { id: "server-other", project_id: "other", name: "其他项目服务器" };
const firstDomain = {
id: "domain-one", project_id: "default", name: "one.example.test",
certificate_type: "single", sans: [], auto_renew: true,
acme_environment: "staging", dns_provider: "",
};
async function mountForm(t, fetcher) {
const deps = {
fetchAdmin: (...args) => fetch(...args),
domains: ref([]),
servers: ref([firstServer, secondServer, otherServer]),
domainServers: ref({ "domain-two": [firstServer, secondServer] }),
notice: ref(""),
loadData: t.mock.fn(async () => {}),
};
const mounted = await mountComposable(t, () => useDomainForm(deps), (path, options) => {
if (path === "/api/v1/dns/accounts") return Promise.resolve(Response.json({ items: [] }));
if (path.endsWith("/dns-account")) return Promise.resolve(Response.json({
id: "", dns_account_id: "", domain_id: path.split("/")[4], project_id: "default",
}));
return fetcher(path, options);
});
return { ...mounted, deps };
}
async function startCreate(state) {
await state.openCreate();
state.form.value.name = firstDomain.name;
state.form.value.server_ids = [firstServer.id, secondServer.id];
}
test("HTTP 验证保存方式且不携带 DNS 服务商", async (t) => {
const requests = [];
const { state } = await mountForm(t, async (path, options) => {
requests.push({ path, ...options });
if (path.endsWith("/servers")) return Response.json({ items: [] });
return Response.json({ ...firstDomain, ...JSON.parse(options.body) });
});
await startCreate(state);
state.form.value.challenge_type = "http-01";
state.form.value.dns_provider = "aliyun";
await state.saveDomain();
assert.equal(JSON.parse(requests[0].body).challenge_type, "http-01");
assert.equal(JSON.parse(requests[0].body).dns_provider, "");
});
test("HTTP 验证拒绝主泛域名和附加泛域名,不提交请求", async (t) => {
let calls = 0;
const { state } = await mountForm(t, async () => { calls += 1; return Response.json({}); });
for (const type of ["wildcard", "san"]) {
await startCreate(state);
state.form.value.challenge_type = "http-01";
state.form.value.certificate_type = type;
state.form.value.sans = "*.example.com";
await state.saveDomain();
assert.match(state.formError.value, /不支持泛域名/);
}
assert.equal(calls, 0);
});
test("泛域名必须填写带通配符的主域名,不提交无效配置", async (t) => {
let calls = 0;
const { state } = await mountForm(t, async () => {
calls += 1;
return Response.json({});
});
await startCreate(state);
state.form.value.certificate_type = "wildcard";
state.form.value.name = "pcux.cn";
await state.saveDomain();
assert.equal(calls, 0);
assert.match(state.formError.value, /泛域名主域名必须以 \*\. 开头/);
state.form.value.name = "*.pcux.cn";
await state.saveDomain();
assert.equal(calls, 1);
});
test("编辑时保留已有 HTTP 验证方式", async (t) => {
const { state } = await mountForm(t, async () => Response.json({ items: [] }));
await state.openEdit({ ...firstDomain, challenge_type: "http-01" });
assert.equal(state.form.value.challenge_type, "http-01");
await state.openEdit(firstDomain);
assert.equal(state.form.value.challenge_type, "dns-01");
});
test("创建成功但关联冲突后只重试关联,不重复创建且不改变另一域名", async (t) => {
const requests = [];
let attempts = 0;
const { state, deps } = await mountForm(t, async (path, options) => {
requests.push({ path, ...options });
if (path === "/api/v1/domains") return Response.json(firstDomain, { status: 201 });
attempts += 1;
return attempts === 1
? Response.json({ error: "共享路径冲突" }, { status: 409 })
: Response.json({ items: [firstServer, secondServer] });
});
await startCreate(state);
await state.saveDomain();
assert.equal(state.editingId.value, firstDomain.id);
assert.equal(state.showForm.value, true);
assert.equal(state.saving.value, false);
assert.deepEqual(state.form.value.server_ids, [firstServer.id, secondServer.id]);
assert.match(state.formError.value, /域名资料已保存.*共享路径冲突/);
assert.deepEqual(deps.domains.value, [firstDomain]);
await state.saveDomain();
assert.deepEqual(requests.map(({ method }) => method), ["POST", "PUT", "PUT"]);
assert.equal(requests[1].path, `/api/v1/domains/${firstDomain.id}/servers`);
assert.equal(requests[2].body, requests[1].body);
assert.deepEqual(deps.domainServers.value["domain-two"], [firstServer, secondServer]);
assert.deepEqual(deps.domainServers.value[firstDomain.id], [firstServer, secondServer]);
assert.equal(state.showForm.value, false);
assert.match(deps.notice.value, /域名已添加/);
assert.equal(deps.loadData.mock.callCount(), 1);
});
test("部分成功后修改资料使用已有标识更新,取消也保留已创建列表项", async (t) => {
const requests = [];
const { state, deps } = await mountForm(t, async (path, options) => {
requests.push({ path, ...options });
if (path.endsWith("/servers")) return Response.json({ error: "关联未保存" }, { status: 403 });
return Response.json({ ...firstDomain, ...JSON.parse(options.body) });
});
await startCreate(state);
await state.saveDomain();
state.form.value.auto_renew = false;
await state.saveDomain();
assert.deepEqual(requests.map(({ method }) => method), ["POST", "PUT", "PUT", "PUT"]);
assert.equal(requests[2].path, `/api/v1/domains/${firstDomain.id}`);
assert.equal(JSON.parse(requests[2].body).auto_renew, false);
state.closeForm();
assert.equal(deps.domains.value.length, 1);
assert.equal(deps.domains.value[0].auto_renew, false);
state.openCreate();
assert.equal(state.editingId.value, "");
assert.deepEqual(state.form.value.server_ids, []);
});
test("编辑先读取真实关联,明确清空只提交当前域名", async (t) => {
const requests = [];
const { state, deps } = await mountForm(t, async (path, options = {}) => {
requests.push({ path, ...options });
if (!options.method) return Response.json({ items: [firstServer, secondServer] });
if (path.endsWith("/servers")) return Response.json({ items: [] });
return Response.json(firstDomain);
});
await state.openEdit(firstDomain);
assert.deepEqual(state.form.value.server_ids, [firstServer.id, secondServer.id]);
state.form.value.server_ids = [];
await state.saveDomain();
assert.equal(requests.length, 3);
assert.deepEqual(JSON.parse(requests[2].body), { server_ids: [] });
assert.deepEqual(deps.domainServers.value[firstDomain.id], []);
assert.deepEqual(deps.domainServers.value["domain-two"], [firstServer, secondServer]);
});
test("服务器按域名项目过滤,不限制其他域名共享,缺归属不默认为默认项目", async (t) => {
const { state, deps } = await mountForm(t, async () => Response.json({ items: [] }));
deps.servers.value.push({ id: "unknown" });
await startCreate(state);
assert.deepEqual(state.domainServerOptions.value.map((server) => server.id), [firstServer.id, secondServer.id]);
await state.openEdit({ ...firstDomain, project_id: "other" });
assert.deepEqual(state.domainServerOptions.value, [otherServer]);
state.form.value.server_ids = [firstServer.id];
await state.saveDomain();
assert.match(state.formError.value, /当前项目/);
await state.openEdit({ ...firstDomain, project_id: "" });
assert.deepEqual(state.domainServerOptions.value, []);
await state.saveDomain();
assert.match(state.formError.value, /项目归属未确认/);
});
for (const failure of ["forbidden", "network", "malformed"]) {
test(`关联读取失败不作为空数组保存,可重新读取:${failure}`, async (t) => {
let reads = 0;
const { state, deps } = await mountForm(t, async (_, options = {}) => {
assert.equal(options.method, undefined);
reads += 1;
if (reads > 1) return Response.json({ items: [firstServer] });
if (failure === "network") throw new Error("offline");
return failure === "forbidden"
? Response.json({ error: "无权限" }, { status: 403 })
: Response.json({});
});
deps.domainServers.value[firstDomain.id] = [firstServer];
await state.openEdit(firstDomain);
assert.equal(state.domainBindingsReady.value, false);
await state.saveDomain();
assert.equal(reads, 1);
assert.deepEqual(deps.domainServers.value[firstDomain.id], [firstServer]);
await state.reloadDomainBindings();
assert.equal(state.domainBindingsReady.value, true);
assert.deepEqual(state.form.value.server_ids, [firstServer.id]);
});
}
test("切换表单时旧关联读取结果不能污染新域名", async (t) => {
const pending = deferred();
const { state, deps } = await mountForm(t, async () => pending.promise);
const reading = state.openEdit(firstDomain);
assert.equal(state.domainBindingsLoading.value, true);
state.closeForm();
state.openCreate();
pending.resolve(Response.json({ items: [firstServer] }));
await reading;
assert.equal(state.editingId.value, "");
assert.deepEqual(state.form.value.server_ids, []);
assert.equal(state.domainBindingsLoading.value, false);
assert.equal(deps.domainServers.value[firstDomain.id], undefined);
});
test("保存期间重复点击、关闭和切换表单不创建额外请求", async (t) => {
const pending = deferred();
let calls = 0;
const { state } = await mountForm(t, async (path) => {
calls += 1;
return path.endsWith("/servers") ? Response.json({ items: [firstServer, secondServer] }) : pending.promise;
});
await startCreate(state);
const saving = state.saveDomain();
await state.saveDomain();
state.openCreate();
await state.openEdit({ ...firstDomain, id: "other-domain" });
state.closeForm();
assert.equal(calls, 1);
assert.equal(state.showForm.value, true);
assert.equal(state.form.value.name, firstDomain.name);
pending.resolve(Response.json(firstDomain));
await saving;
assert.equal(calls, 2);
});
for (const failure of ["network", "server", "malformed", "missing-project"]) {
test(`资料保存结果不明确时禁止盲目重新创建:${failure}`, async (t) => {
let calls = 0;
const { state } = await mountForm(t, async () => {
calls += 1;
if (failure === "network") throw new Error("offline");
if (failure === "server") return Response.json({}, { status: 500 });
return Response.json(failure === "missing-project" ? { id: firstDomain.id } : {});
});
await startCreate(state);
await state.saveDomain();
assert.equal(state.domainSaveUncertain.value, true);
assert.match(state.formError.value, /刷新列表确认/);
await state.saveDomain();
assert.equal(calls, 1);
});
}
test("卸载后不继续保存关联,不改写列表", async (t) => {
const pending = deferred();
let calls = 0;
const { state, deps, unmount } = await mountForm(t, async () => {
calls += 1;
return pending.promise;
});
await startCreate(state);
const saving = state.saveDomain();
unmount();
pending.resolve(Response.json(firstDomain));
await saving;
assert.equal(calls, 1);
assert.deepEqual(deps.domains.value, []);
});
test("关联网络失败可重试,但刷新失败不能被误报为保存失败", async (t) => {
let bindings = 0;
const { state, deps } = await mountForm(t, async (path) => {
if (!path.endsWith("/servers")) return Response.json(firstDomain);
bindings += 1;
if (bindings === 1) throw new Error("offline");
return Response.json({ items: [firstServer, secondServer] });
});
deps.loadData.mock.mockImplementation(async () => { throw new Error("offline"); });
await startCreate(state);
await state.saveDomain();
assert.match(state.formError.value, /关联保存结果未确认/);
assert.equal(state.domainSaveUncertain.value, false);
await state.saveDomain();
assert.equal(state.showForm.value, false);
assert.equal(state.formError.value, "");
assert.match(deps.notice.value, /配置已保存.*列表刷新失败/);
});