Scert/backend/internal/dnsprovider/aliyun_authorization_verify_test.go

228 lines
7.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, ",")
}