Scert/frontend/src/views/Dashboard/DomainDNSAccount.test.js

262 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 account = {
id: "account-one", project_id: "default", provider: "aliyun", provider_name: "阿里云 DNS",
name: "生产账户", owner_username: "甲用户", managed: true, status: "active",
verification_state: "verified", credential_state: "available", revision: 3,
};
const item = {
id: "domain-one", project_id: "default", name: "one.example.test", sans: [],
certificate_type: "single", auto_renew: true, acme_environment: "staging",
challenge_type: "dns-01", dns_provider: "aliyun",
};
const emptyBinding = { id: "", domain_id: item.id, project_id: "default", dns_account_id: "" };
const originalBinding = { ...emptyBinding, id: "binding-old", dns_account_id: account.id };
async function mountForm(t, { accounts = [account], binding = emptyBinding, intercept = () => null } = {}) {
const requests = [];
let currentBinding = { ...binding };
const deps = {
fetchAdmin: (...args) => fetch(...args), domains: ref([]), servers: ref([]),
domainServers: ref({}), notice: ref(""), loadData: async () => {},
};
const mounted = await mountComposable(t, () => useDomainForm(deps), async (path, options = {}) => {
requests.push({ path, ...options });
const intercepted = intercept(path, options);
if (intercepted) return intercepted;
if (path === "/api/v1/dns/accounts") return Response.json({ items: accounts });
if (path.endsWith("/dns-account")) {
if (options.method) {
const body = JSON.parse(options.body);
currentBinding = {
...emptyBinding, id: body.dns_account_id ? "binding-new" : "", dns_account_id: body.dns_account_id,
};
}
return Response.json(currentBinding);
}
if (path.endsWith("/servers")) return Response.json({ items: [] });
return Response.json({ ...item, ...JSON.parse(options.body) });
});
return { ...mounted, deps, requests, writes: () => requests.filter((request) => request.method) };
}
async function create(state) {
await state.openCreate();
state.form.value.name = item.name;
state.dnsAccountID.value = account.id;
}
test("新域名先保存资料再显式绑定账户,提交真实版本和空绑定令牌", async (t) => {
const { state, deps, writes } = await mountForm(t);
await create(state);
await state.saveDomain();
assert.deepEqual(writes().map((request) => request.path), [
"/api/v1/domains", `/api/v1/domains/${item.id}/dns-account`, `/api/v1/domains/${item.id}/servers`,
]);
assert.equal(JSON.parse(writes()[0].body).dns_provider, "aliyun");
assert.deepEqual(JSON.parse(writes()[1].body), {
dns_account_id: account.id, expected_binding_id: "", account_revision: 3,
});
assert.equal(deps.domains.value[0].dns_binding_id, "binding-new");
assert.equal(state.showForm.value, false);
});
test("编辑使用真实绑定令牌,更换服务商时先保留旧服务商再事务绑定", async (t) => {
const second = { ...account, id: "account-two", provider: "cloudflare", revision: 8 };
const { state, deps, writes } = await mountForm(t, { accounts: [account, second], binding: originalBinding });
await state.openEdit({ ...item, dns_binding_id: "stale-list" });
state.dnsAccountID.value = second.id;
await state.saveDomain();
assert.equal(JSON.parse(writes()[0].body).dns_provider, "aliyun");
assert.deepEqual(JSON.parse(writes()[1].body), {
dns_account_id: second.id, expected_binding_id: originalBinding.id, account_revision: 8,
});
assert.equal(deps.domains.value[0].dns_provider, "cloudflare");
});
test("账户按项目、状态、验证结果、凭据可用性及真实独立标识筛选", async (t) => {
const invalid = [
{ project_id: "other" }, { managed: false }, { status: "disabled" },
{ verification_state: "unverified" }, { credential_state: "missing" },
{ revision: 0 }, { revision: 2147483648 }, { provider: "../bad" }, { id: "../bad" },
].map((overrides, index) => ({ ...account, id: `invalid-${index}`, ...overrides }));
const { state, writes } = await mountForm(t, { accounts: [null, ...invalid, account] });
await create(state);
assert.deepEqual(state.domainDNSAccountOptions.value.map((entry) => entry.id), [account.id]);
for (const entry of invalid) {
state.dnsAccountID.value = entry.id;
await state.saveDomain();
assert.match(state.formError.value, /当前项目内/);
}
assert.equal(writes().length, 0);
});
test("停用的原账户保留绑定且不重新使用旧验证结果,可以显式解除", async (t) => {
const { state, writes } = await mountForm(t, { accounts: [{ ...account, status: "disabled" }], binding: originalBinding });
await state.openEdit(item);
assert.equal(state.dnsAccountID.value, account.id);
assert.equal(state.dnsUnavailableSelection.value, true);
state.dnsAccountID.value = "";
await state.saveDomain();
assert.deepEqual(JSON.parse(writes()[1].body), {
dns_account_id: "", expected_binding_id: originalBinding.id, account_revision: 0,
});
});
test("已有绑定切换 HTTP 验证必须先解除,不能在同次保存中静默回退", async (t) => {
const { state, writes } = await mountForm(t, { binding: originalBinding });
await state.openEdit(item);
state.dnsAccountID.value = "";
state.form.value.challenge_type = "http-01";
await state.saveDomain();
assert.match(state.formError.value, /先.*解除账户绑定/);
assert.equal(writes().length, 0);
});
test("无绑定 HTTP 验证保留兼容路径,不调用绑定写入", async (t) => {
const { state, writes } = await mountForm(t);
await state.openCreate();
state.form.value.name = item.name;
state.form.value.challenge_type = "http-01";
await state.saveDomain();
assert.equal(writes().length, 2);
assert.equal(JSON.parse(writes()[0].body).dns_provider, "");
});
for (const failure of ["network", "invalid-json", "wrong-domain", "wrong-project", "half-binding", "forbidden"]) {
test(`绑定读取失败不清空、不允许保存:${failure}`, async (t) => {
const { state, writes } = await mountForm(t, { intercept: (path) => {
if (!path.endsWith("/dns-account")) return null;
if (failure === "network") return Promise.reject(new Error("secret-test-only"));
if (failure === "invalid-json") return new Response("secret-test-only");
if (failure === "forbidden") return Response.json({ error: "secret-test-only" }, { status: 403 });
return Response.json({ ...originalBinding,
...(failure === "wrong-domain" ? { domain_id: "other-domain" } : {}),
...(failure === "wrong-project" ? { project_id: "other" } : {}),
...(failure === "half-binding" ? { dns_account_id: "" } : {}),
});
} });
await state.openEdit(item);
assert.equal(state.dnsBindingReady.value, false);
assert.doesNotMatch(state.dnsBindingError.value, /secret-test-only/);
await state.saveDomain();
assert.equal(writes().length, 0);
});
}
for (const failure of ["network", "conflict", "forbidden", "server", "invalid-json", "wrong-domain", "wrong-account"]) {
test(`绑定写入失败停止后续关联且禁止盲目重放:${failure}`, async (t) => {
const { state, writes } = await mountForm(t, { intercept: (path, options) => {
if (!path.endsWith("/dns-account") || !options.method) return null;
if (failure === "network") return Promise.reject(new Error("secret-test-only"));
if (failure === "invalid-json") return new Response("secret-test-only");
if (["conflict", "forbidden", "server"].includes(failure)) {
return Response.json({ error: "secret-test-only" }, { status: { conflict: 409, forbidden: 403, server: 503 }[failure] });
}
return Response.json({ ...originalBinding,
...(failure === "wrong-domain" ? { domain_id: "other-domain" } : { dns_account_id: "other-account" }),
});
} });
await create(state);
await state.saveDomain();
assert.equal(writes().length, 2);
assert.equal(state.dnsBindingReady.value, false);
assert.match(state.formError.value, /域名资料已保存.*DNS 账户绑定/);
assert.doesNotMatch(state.formError.value, /secret-test-only/);
await state.saveDomain();
assert.equal(writes().length, 2);
});
}
test("绑定已成功而服务器关联失败时只重试服务器,不重新创建或绑定", async (t) => {
let calls = 0;
const { state, writes } = await mountForm(t, { intercept: (path, options) => {
if (!path.endsWith("/servers") || !options.method) return null;
calls += 1;
return calls === 1 ? Response.json({ error: "关联冲突" }, { status: 409 }) : null;
} });
await create(state);
await state.saveDomain();
assert.equal(writes().length, 3);
assert.equal(state.showForm.value, true);
await state.saveDomain();
assert.equal(writes().length, 4);
assert.ok(writes()[3].path.endsWith("/servers"));
assert.equal(state.showForm.value, false);
});
test("冲突后重新读取实际绑定,确认状态而不是自动重放旧选择", async (t) => {
let conflict = true;
const { state, writes } = await mountForm(t, { intercept: (path, options) => {
if (!path.endsWith("/dns-account")) return null;
if (options.method && conflict) {
conflict = false;
return Response.json({}, { status: 409 });
}
if (!options.method) return Response.json(originalBinding);
return null;
} });
await create(state);
await state.saveDomain();
await state.reloadDomainDNS();
assert.equal(state.dnsBindingReady.value, true);
assert.equal(state.dnsAccountID.value, account.id);
await state.saveDomain();
assert.equal(writes().filter((request) => request.path.endsWith("/dns-account")).length, 1);
assert.equal(writes().filter((request) => request.method === "POST").length, 1);
});
test("切换或卸载后迟到的账户响应不能恢复旧表单状态", async (t) => {
const pending = deferred();
let reads = 0;
const { state, unmount, writes } = await mountForm(t, { intercept: (path) => {
if (path !== "/api/v1/dns/accounts") return null;
reads += 1;
return reads === 1 ? pending.promise : null;
} });
const opening = state.openEdit(item);
assert.equal(state.dnsBindingLoading.value, true);
state.closeForm();
await state.openCreate();
pending.resolve(Response.json({ items: [{ ...account, id: "late-account" }] }));
await opening;
assert.deepEqual(state.domainDNSAccountOptions.value.map((entry) => entry.id), [account.id]);
assert.equal(state.dnsAccountID.value, "");
unmount();
assert.equal(state.dnsBindingReady.value, false);
assert.equal(writes().length, 0);
});
test("保存中固定目标账户和版本,拒绝双击、关闭、重读和切换", async (t) => {
const pending = deferred();
const { state, writes } = await mountForm(t, { intercept: (path) => path === "/api/v1/domains" ? pending.promise : null });
await create(state);
const saving = state.saveDomain();
state.dnsAccountID.value = "forged-account";
state.editingId.value = "forged-domain";
state.closeForm();
await state.openCreate();
await state.reloadDomainDNS();
await state.saveDomain();
assert.equal(writes().length, 1);
pending.resolve(Response.json(item));
await saving;
assert.equal(writes()[1].path, `/api/v1/domains/${item.id}/dns-account`);
assert.equal(JSON.parse(writes()[1].body).dns_account_id, account.id);
});
test("卸载后返回的资料响应不继续写 DNS 绑定或服务器", async (t) => {
const pending = deferred();
const { state, unmount, writes } = await mountForm(t, { intercept: (path) => path === "/api/v1/domains" ? pending.promise : null });
await create(state);
const saving = state.saveDomain();
unmount();
pending.resolve(Response.json(item));
await saving;
assert.equal(writes().length, 1);
});