package dnsprovider import ( "context" "errors" "strings" "testing" "time" ) type authorizationCall struct { name string id string } type authorizationRecordAPI struct { calls []authorizationCall zone string rr string value string addErr error describeErr error deleteErr error describeInfo aliyunRecordInfo cancelAfterAdd context.CancelFunc cleanupWasCanceled bool callTimeouts []time.Duration } func (a *authorizationRecordAPI) Add(ctx context.Context, zone, rr, value string) (string, error) { a.recordCall("add", "", ctx) a.zone, a.rr, a.value = zone, rr, value if a.cancelAfterAdd != nil { a.cancelAfterAdd() } if a.addErr != nil { return "", a.addErr } return "authorization-record-id", nil } func (a *authorizationRecordAPI) Describe(ctx context.Context, zone, rr, recordID string) (aliyunRecordInfo, error) { if zone != a.zone || rr != a.rr { return aliyunRecordInfo{}, errors.New("查询必须限定本次创建记录的托管区和主机记录") } a.recordCall("describe", recordID, ctx) if a.describeErr != nil { return aliyunRecordInfo{}, a.describeErr } if err := ctx.Err(); err != nil { return aliyunRecordInfo{}, err } if a.describeInfo.RecordID != "" { return a.describeInfo, nil } return aliyunRecordInfo{ RecordID: recordID, DomainName: a.zone, RR: a.rr, Type: "TXT", Value: a.value, }, nil } func (a *authorizationRecordAPI) Delete(ctx context.Context, recordID string) error { a.recordCall("delete", recordID, ctx) a.cleanupWasCanceled = ctx.Err() != nil return a.deleteErr } func (a *authorizationRecordAPI) recordCall(name, id string, ctx context.Context) { a.calls = append(a.calls, authorizationCall{name: name, id: id}) timeout := time.Duration(0) if deadline, ok := ctx.Deadline(); ok { timeout = time.Until(deadline) } a.callTimeouts = append(a.callTimeouts, timeout) } func newAuthorizationCheckerForTest(api aliyunRecordAPI) *AliyunAuthorizationChecker { checker := newAliyunAuthorizationCheckerForTest() checker.recordAPI = func(map[string]string) (aliyunRecordAPI, error) { return api, nil } return checker } func TestAliyunAuthorizationVerifyClosesWriteReadCleanupLoop(t *testing.T) { api := &authorizationRecordAPI{} report, err := newAuthorizationCheckerForTest(api).Verify(t.Context(), "www.sub.example.com", scopeCredentials()) if err != nil { t.Fatalf("阿里云授权闭环验证失败:%v", err) } if !report.Verified || report.RecordName == "" || len(report.Domains) != 1 { t.Fatalf("授权报告不完整:%+v", report) } domain := report.Domains[0] if !domain.Writable || !domain.Readable || !domain.Cleanup || domain.Status != "可操作" { t.Fatalf("授权闭环状态不正确:%+v", domain) } if got := callNames(api.calls); got != "add,describe,delete" { t.Fatalf("API 调用顺序错误:%s", got) } if api.calls[1].id != "authorization-record-id" || api.calls[2].id != "authorization-record-id" { t.Fatalf("查询或清理未使用本次创建的记录 ID:%+v", api.calls) } if len(api.callTimeouts) != 3 { t.Fatalf("未记录全部 API 调用时限:%+v", api.callTimeouts) } for index, timeout := range api.callTimeouts { if timeout <= 0 || timeout > aliyunAPICallTimeout { t.Fatalf("第 %d 次阿里云 API 调用未限制在 %s 内:%s", index+1, aliyunAPICallTimeout, timeout) } } } func TestAliyunAuthorizationVerifyDoesNotCallCloudForProtectedDomain(t *testing.T) { api := &authorizationRecordAPI{} checker := newAuthorizationCheckerForTest(api) constructed := 0 checker.recordAPI = func(map[string]string) (aliyunRecordAPI, error) { constructed++ return api, nil } report, err := checker.Verify(t.Context(), "pcux.cn", scopeCredentials()) if err == nil || report.Verified || constructed != 0 || len(api.calls) != 0 { t.Fatalf("范围外域名不应构造客户端或调用云 API:report=%+v constructed=%d calls=%+v err=%v", report, constructed, api.calls, err) } } func TestAliyunAuthorizationVerifyFailurePathsAlwaysPreserveProofBoundary(t *testing.T) { tests := []struct { name string configure func(*authorizationRecordAPI) wantCalls string wantWritable bool wantReadable bool wantCleanup bool wantErrorPart string }{ { name: "创建失败", configure: func(api *authorizationRecordAPI) { api.addErr = errors.New("Forbidden.RAM") }, wantCalls: "add", wantErrorPart: "Forbidden.RAM", }, { name: "查询失败仍清理", configure: func(api *authorizationRecordAPI) { api.describeErr = errors.New("Forbidden.RAM") }, wantCalls: "add,describe,delete", wantWritable: true, wantCleanup: true, wantErrorPart: "查询授权探测 TXT 记录失败", }, { name: "查询内容不一致仍清理", configure: func(api *authorizationRecordAPI) { api.describeInfo = aliyunRecordInfo{RecordID: "other-record-id", DomainName: "sub.example.com", RR: "wrong", Type: "TXT", Value: "wrong"} }, wantCalls: "add,describe,delete", wantWritable: true, wantCleanup: true, wantErrorPart: "不一致", }, { name: "清理失败不能通过", configure: func(api *authorizationRecordAPI) { api.deleteErr = errors.New("Forbidden.RAM") }, wantCalls: "add,describe,delete", wantWritable: true, wantReadable: true, wantErrorPart: "清理授权探测 TXT 记录失败", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { api := &authorizationRecordAPI{} tc.configure(api) report, err := newAuthorizationCheckerForTest(api).Verify(t.Context(), "www.sub.example.com", scopeCredentials()) if err == nil || !strings.Contains(err.Error(), tc.wantErrorPart) { t.Fatalf("错误结果不正确:err=%v", err) } if report.Verified || len(report.Domains) != 1 { t.Fatalf("失败路径不能报告已验证:%+v", report) } domain := report.Domains[0] if domain.Writable != tc.wantWritable || domain.Readable != tc.wantReadable || domain.Cleanup != tc.wantCleanup { t.Fatalf("失败路径状态错误:%+v", domain) } if got := callNames(api.calls); got != tc.wantCalls { t.Fatalf("失败路径 API 调用顺序错误:got=%s want=%s", got, tc.wantCalls) } }) } } func TestAliyunAuthorizationVerifyCleansUpAfterRequestCancellation(t *testing.T) { ctx, cancel := context.WithCancel(t.Context()) api := &authorizationRecordAPI{cancelAfterAdd: cancel} report, err := newAuthorizationCheckerForTest(api).Verify(ctx, "www.sub.example.com", scopeCredentials()) if !errors.Is(err, context.Canceled) || report.Verified { t.Fatalf("请求取消必须失败且不能报告已验证:report=%+v err=%v", report, err) } if got := callNames(api.calls); got != "add,describe,delete" { t.Fatalf("请求取消后必须完成受控清理:%s", got) } if api.cleanupWasCanceled { t.Fatal("清理请求不应继承已取消的上下文") } } func TestPermissionVerificationTimeoutForAliyunIsIndependentFromPropagation(t *testing.T) { if got := PermissionVerificationTimeoutFor(Aliyun); got != 6*time.Second { t.Fatalf("阿里云权限验证时限错误:%s", got) } if got := PermissionVerificationTimeoutFor(Cloudflare); got != 60*time.Second { t.Fatalf("其他 DNS 服务商权限验证时限错误:%s", got) } } func callNames(calls []authorizationCall) string { names := make([]string, 0, len(calls)) for _, call := range calls { names = append(names, call.name) } return strings.Join(names, ",") }