补充 DNS 验证等待测试

This commit is contained in:
xuans 2026-09-15 23:34:56 +08:00
parent ddbf3b2d29
commit 519a538755
6 changed files with 149 additions and 18 deletions

View File

@ -55,10 +55,10 @@ func TestAdminDNSTestReturnsVerificationResult(t *testing.T) {
t.Fatalf("DNS 在线测试失败:%d %s", response.Code, response.Body.String())
}
var payload struct {
Verified bool `json:"verified"`
CredentialRef string `json:"credential_ref"`
VerificationToken string `json:"verification_token"`
TestReport setup.DNSVerificationReport `json:"test_report"`
Verified bool `json:"verified"`
CredentialRef string `json:"credential_ref"`
VerificationToken string `json:"verification_token"`
TestReport setup.DNSVerificationReport `json:"test_report"`
VerificationTimeoutSecs int `json:"verification_timeout_secs"`
}
if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil {
@ -80,4 +80,7 @@ func TestAdminDNSTestRejectsUnavailableService(t *testing.T) {
if response.Code != http.StatusServiceUnavailable {
t.Fatalf("DNS 服务未初始化时应返回 503,实际为 %d:%s", response.Code, response.Body.String())
}
if !strings.Contains(response.Body.String(), `"verification_timeout_secs":930`) {
t.Fatalf("DNS 服务未初始化响应应包含当前最大等待时间:%s", response.Body.String())
}
}

View File

@ -159,12 +159,12 @@ func (s *Server) testAdminDNS(c *gin.Context) {
func (s *Server) testDNS(c *gin.Context) {
verificationTimeoutSecs := int(s.dnsVerificationTimeout().Seconds())
if s.setupDNS == nil {
s.writeError(c, http.StatusServiceUnavailable, "DNS 在线验证服务尚未初始化", nil)
s.writeDNSTestError(c, http.StatusServiceUnavailable, "DNS 在线验证服务尚未初始化", nil, verificationTimeoutSecs)
return
}
var request dnsTestRequest
if err := bindJSON(c, &request, setupRequestLimit); err != nil {
s.writeError(c, http.StatusBadRequest, "DNS 测试请求参数无效", err)
s.writeDNSTestError(c, http.StatusBadRequest, "DNS 测试请求参数无效", err, verificationTimeoutSecs)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), s.dnsVerificationTimeout())
@ -206,6 +206,16 @@ func (s *Server) testDNS(c *gin.Context) {
})
}
func (s *Server) writeDNSTestError(c *gin.Context, status int, message string, err error, verificationTimeoutSecs int) {
if s.logger != nil {
s.logger.Error(message, "error", err)
}
c.JSON(status, gin.H{
"error": message,
"verification_timeout_secs": verificationTimeoutSecs,
})
}
func (s *Server) dnsVerificationTimeout() time.Duration {
s.configMu.RLock()
cfg := s.config

View File

@ -527,6 +527,9 @@ func TestSetupDNSTestRejectsRemoteAndOversizedRequests(t *testing.T) {
if oversizedResponse.Code != http.StatusBadRequest {
t.Fatalf("超大 DNS 测试请求应被拒绝:%d %s", oversizedResponse.Code, oversizedResponse.Body.String())
}
if !strings.Contains(oversizedResponse.Body.String(), `"verification_timeout_secs":930`) {
t.Fatalf("DNS 参数错误响应应包含当前最大等待时间:%s", oversizedResponse.Body.String())
}
}
func TestSetupDNSTestReturnsNotImplementedForUnavailableProvider(t *testing.T) {
@ -544,6 +547,9 @@ func TestSetupDNSTestReturnsNotImplementedForUnavailableProvider(t *testing.T) {
if strings.Contains(response.Body.String(), "access_key_secret") || strings.Contains(response.Body.String(), "secret") {
t.Fatal("未开放 Provider 响应不应包含提交的凭据")
}
if !strings.Contains(response.Body.String(), `"verification_timeout_secs":930`) {
t.Fatalf("未开放 Provider 响应应包含当前最大等待时间:%s", response.Body.String())
}
}
func TestSetupValidationFailureDoesNotConsumeDNSToken(t *testing.T) {

View File

@ -30,6 +30,7 @@ export function verifiedResponse(token = "test-token") {
export async function mountComposable(t, factory, fetcher) {
const intervals = new Map();
let nextIntervalID = 1;
let currentTime = Date.now();
const originals = new Map();
for (const [key, value] of Object.entries({
window: {
@ -49,6 +50,7 @@ export async function mountComposable(t, factory, fetcher) {
originals.set(key, Object.getOwnPropertyDescriptor(globalThis, key));
Object.defineProperty(globalThis, key, { configurable: true, writable: true, value });
}
t.mock.method(Date, "now", () => currentTime);
t.mock.method(globalThis, "fetch", fetcher);
const renderer = createRenderer({
createComment: () => ({}),
@ -85,5 +87,8 @@ export async function mountComposable(t, factory, fetcher) {
runIntervals() {
for (const callback of [...intervals.values()]) callback();
},
advanceTime(milliseconds) {
currentTime += milliseconds;
},
};
}

View File

@ -5,15 +5,16 @@ import { useDashboard } from "./Dashboard.js";
import { aliyunProvider, deferred, mountComposable, verifiedResponse } from "../../test-support/mount.js";
async function setup(t, request) {
const { state } = await mountComposable(t, useDashboard, async (path) => {
const mounted = await mountComposable(t, useDashboard, async (path) => {
if (path === "/api/v1/dns/test") return request();
if (path.endsWith("/dns/providers")) return Response.json({ items: [aliyunProvider] });
if (path.endsWith("/settings")) return Response.json({ dns: { provider: "aliyun", credential_configured: true, verification_state: "verified" } });
return Response.json({ items: [] });
});
const { state } = mounted;
state.openSettings();
await nextTick();
return state;
return mounted;
}
async function fill(state) {
@ -23,7 +24,7 @@ async function fill(state) {
}
test("已保存凭据不变可保存,输入新凭据必须重新验证", async (t) => {
const state = await setup(t, () => verifiedResponse());
const { state } = await setup(t, () => verifiedResponse());
assert.equal(state.dnsReadyToSave.value, true);
await fill(state);
assert.equal(state.dnsReadyToSave.value, false);
@ -35,7 +36,7 @@ test("已保存凭据不变可保存,输入新凭据必须重新验证", async
test("管理设置不接受旧输入的在途 DNS 响应", async (t) => {
const pending = deferred();
const state = await setup(t, () => pending.promise);
const { state } = await setup(t, () => pending.promise);
await fill(state);
const task = state.testDNS();
state.settingsForm.value.dns.test_domain = "other.test";
@ -47,14 +48,14 @@ test("管理设置不接受旧输入的在途 DNS 响应", async (t) => {
});
test("管理设置拒绝过期验证令牌", async (t) => {
const state = await setup(t, () => Response.json({ credential_ref: "secret://test/only", verification_token: "expired", expires_at: "2000-01-01T00:00:00Z" }));
const { state } = await setup(t, () => Response.json({ credential_ref: "secret://test/only", verification_token: "expired", expires_at: "2000-01-01T00:00:00Z" }));
await fill(state);
await state.testDNS();
assert.equal(state.dnsReadyToSave.value, false);
});
test("管理设置变更托管区不能复用验证令牌", async (t) => {
const state = await setup(t, () => verifiedResponse());
const { state } = await setup(t, () => verifiedResponse());
await fill(state);
state.settingsForm.value.dns.credentials.managed_zones = "example.test";
state.settingsForm.value.dns.credentials.allowed_domains = "example.test";
@ -67,7 +68,7 @@ test("管理设置变更托管区不能复用验证令牌", async (t) => {
test("关闭设置后不显示旧 DNS 请求的成功提示", async (t) => {
const pending = deferred();
const state = await setup(t, () => pending.promise);
const { state } = await setup(t, () => pending.promise);
await fill(state);
const task = state.testDNS();
state.closeSettings();
@ -81,7 +82,7 @@ test("管理设置新请求成功后旧失败不能删除令牌", async (t) => {
const first = deferred();
const second = deferred();
let count = 0;
const state = await setup(t, () => (++count === 1 ? first.promise : second.promise));
const { state } = await setup(t, () => (++count === 1 ? first.promise : second.promise));
await fill(state);
const oldTask = state.testDNS();
const newTask = state.testDNS();
@ -93,3 +94,52 @@ test("管理设置新请求成功后旧失败不能删除令牌", async (t) => {
assert.equal(state.dnsTestError.value, "");
assert.equal(state.dnsReadyToSave.value, true);
});
test("管理设置 DNS 验证显示等待进度,并在结束后停止计时", async (t) => {
const pending = deferred();
const { state, runIntervals, advanceTime } = await setup(t, () => pending.promise);
await fill(state);
const task = state.testDNS();
assert.equal(state.dnsTesting.value, true);
assert.equal(state.dnsVerificationElapsedSeconds.value, 0);
assert.equal(state.dnsVerificationTimeoutSeconds.value, 930);
advanceTime(3200);
runIntervals();
assert.equal(state.dnsVerificationElapsedSeconds.value, 3);
pending.resolve(Response.json({
verified: true,
credential_ref: "secret://test/only",
verification_token: "test-token",
expires_at: new Date(Date.now() + 60000).toISOString(),
verification_timeout_secs: 31,
}));
await task;
assert.equal(state.dnsTesting.value, false);
assert.equal(state.dnsVerificationElapsedSeconds.value, 0);
assert.equal(state.dnsVerificationTimeoutSeconds.value, 31);
advanceTime(1000);
runIntervals();
assert.equal(state.dnsVerificationElapsedSeconds.value, 0);
});
test("关闭管理设置会停止 DNS 等待计时器", async (t) => {
const pending = deferred();
const { state, runIntervals, advanceTime } = await setup(t, () => pending.promise);
await fill(state);
const task = state.testDNS();
advanceTime(1000);
runIntervals();
assert.equal(state.dnsVerificationElapsedSeconds.value, 1);
state.closeSettings();
assert.equal(state.dnsTesting.value, false);
assert.equal(state.dnsVerificationElapsedSeconds.value, 0);
advanceTime(1000);
runIntervals();
assert.equal(state.dnsVerificationElapsedSeconds.value, 0);
pending.resolve(verifiedResponse());
await task;
});

View File

@ -18,14 +18,45 @@ test("已有初始化服务商选择保持兼容", async (t) => {
});
async function setup(t, request) {
const { state, unmount } = await mountComposable(t, () => useSetupWizard({ status: { dns_provider: "aliyun" } }, () => {}),
const mounted = await mountComposable(t, () => useSetupWizard({ status: { dns_provider: "aliyun" } }, () => {}),
async (path) => path.endsWith("/providers") ? Response.json({ items: [aliyunProvider] }) : request());
const { state } = mounted;
state.dnsCredentials.value = { access_key_id: "fake-key" };
state.dnsTestDomain.value = "example.test";
await nextTick();
return { state, unmount };
return mounted;
}
test("初始化 DNS 验证显示等待进度,并采用服务端返回的最长等待时间", async (t) => {
const pending = deferred();
const { state, runIntervals, advanceTime } = await setup(t, () => pending.promise);
const task = state.testDNS();
assert.equal(state.dnsTesting.value, true);
assert.equal(state.dnsVerificationElapsedSeconds.value, 0);
assert.equal(state.dnsVerificationTimeoutSeconds.value, 930);
advanceTime(2500);
runIntervals();
assert.equal(state.dnsVerificationElapsedSeconds.value, 2);
pending.resolve(Response.json({
verified: true,
credential_ref: "secret://test/only",
verification_token: "test-token",
expires_at: new Date(Date.now() + 60000).toISOString(),
verification_timeout_secs: 31,
}));
await task;
assert.equal(state.dnsTesting.value, false);
assert.equal(state.dnsVerificationElapsedSeconds.value, 0);
assert.equal(state.dnsVerificationTimeoutSeconds.value, 31);
advanceTime(1000);
runIntervals();
assert.equal(state.dnsVerificationElapsedSeconds.value, 0);
});
test("初始化不接受输入修改前的在途 DNS 响应", async (t) => {
const pending = deferred();
const { state } = await setup(t, () => pending.promise);
@ -61,6 +92,25 @@ test("初始化当前 DNS 成功有效,修改域名立即失效", async (t) =>
assert.equal(state.dnsVerificationValid.value, false);
});
test("初始化修改 DNS 输入后会停止等待计时器", async (t) => {
const pending = deferred();
const { state, runIntervals, advanceTime } = await setup(t, () => pending.promise);
const task = state.testDNS();
advanceTime(1000);
runIntervals();
assert.equal(state.dnsVerificationElapsedSeconds.value, 1);
state.dnsTestDomain.value = "other.test";
assert.equal(state.dnsTesting.value, false);
assert.equal(state.dnsVerificationElapsedSeconds.value, 0);
advanceTime(1000);
runIntervals();
assert.equal(state.dnsVerificationElapsedSeconds.value, 0);
pending.resolve(verifiedResponse());
await task;
});
test("初始化修改显式授权范围立即作废旧验证", async (t) => {
const { state } = await setup(t, () => verifiedResponse());
state.dnsCredentials.value.managed_zones = "example.test";
@ -72,11 +122,18 @@ test("初始化修改显式授权范围立即作废旧验证", async (t) => {
assert.equal(state.dnsVerification.value, null);
});
test("初始化卸载后不恢复旧 DNS 验证结果", async (t) => {
test("初始化卸载后不恢复旧 DNS 验证结果或继续等待计时", async (t) => {
const pending = deferred();
const { state, unmount } = await setup(t, () => pending.promise);
const { state, unmount, runIntervals, advanceTime } = await setup(t, () => pending.promise);
const task = state.testDNS();
advanceTime(1000);
runIntervals();
assert.equal(state.dnsVerificationElapsedSeconds.value, 1);
unmount();
assert.equal(state.dnsVerificationElapsedSeconds.value, 0);
advanceTime(1000);
runIntervals();
assert.equal(state.dnsVerificationElapsedSeconds.value, 0);
pending.resolve(verifiedResponse());
await task;
assert.equal(state.dnsVerification.value, null);