Scert/backend/internal/dnsprovider/tester_test.go

286 lines
8.8 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"
"github.com/go-acme/lego/v5/challenge"
"github.com/go-acme/lego/v5/challenge/dns01"
)
type fakeFactory struct {
provider challenge.Provider
err error
}
func (f fakeFactory) Create(string, map[string]string) (challenge.Provider, error) {
return f.provider, f.err
}
type fakeProvider struct {
presented bool
cleaned bool
cleanupSawCanceled bool
presentErr error
cleanupErr error
domain string
keyAuth string
}
type fakeTXTResolver struct {
provider *fakeProvider
}
func (r fakeTXTResolver) LookupTXT(ctx context.Context, _ string) ([]string, error) {
if r.provider == nil || r.provider.domain == "" || r.provider.keyAuth == "" {
return nil, errors.New("测试 DNS 记录尚未创建")
}
return []string{dns01.GetChallengeInfo(ctx, r.provider.domain, r.provider.keyAuth).Value}, nil
}
func (p *fakeProvider) Present(_ context.Context, domain, _, keyAuth string) error {
p.presented = true
p.domain = domain
p.keyAuth = keyAuth
return p.presentErr
}
func (p *fakeProvider) CleanUp(ctx context.Context, _ string, _ string, _ string) error {
p.cleaned = true
p.cleanupSawCanceled = ctx.Err() != nil
return p.cleanupErr
}
type missingTXTResolver struct{}
func (missingTXTResolver) LookupTXT(context.Context, string) ([]string, error) {
return nil, nil
}
func TestTesterCreatesAndCleansChallengeRecord(t *testing.T) {
t.Setenv("LEGO_DISABLE_CNAME_SUPPORT", "true")
provider := &fakeProvider{}
tester := NewTesterWithResolver(fakeFactory{provider: provider}, fakeTXTResolver{provider: provider})
report, err := tester.Verify(t.Context(), Cloudflare, "example.com", map[string]string{"api_token": "secret-token"})
if err != nil {
t.Fatalf("DNS 测试失败:%v", err)
}
if !provider.presented || !provider.cleaned {
t.Fatalf("DNS 测试未完整执行:presented=%v cleaned=%v", provider.presented, provider.cleaned)
}
if report.RecordName == "" || len(report.Stages) != 3 {
t.Fatalf("DNS 测试阶段报告不完整:%+v", report)
}
for index, stage := range report.Stages {
if stage.Status != "completed" && !(index == 2 && stage.Status == "warning") {
t.Fatalf("DNS 测试阶段状态异常:%+v", report.Stages)
}
}
}
func TestTesterUsesConfiguredPropagationSettings(t *testing.T) {
t.Setenv("LEGO_DISABLE_CNAME_SUPPORT", "true")
provider := &fakeProvider{}
tester := NewTesterWithResolverAndSettings(fakeFactory{provider: provider}, missingTXTResolver{}, 20*time.Millisecond, 5*time.Millisecond)
started := time.Now()
_, err := tester.Verify(context.Background(), Cloudflare, "example.com", map[string]string{"api_token": "secret-token"})
if err == nil {
t.Fatal("missing TXT record should fail")
}
if elapsed := time.Since(started); elapsed > time.Second {
t.Fatalf("configured propagation timeout was ignored: %s", elapsed)
}
if !provider.cleaned {
t.Fatal("failed propagation check should clean up the record")
}
}
func TestTesterDoesNotCleanupWhenPresentFails(t *testing.T) {
t.Setenv("LEGO_DISABLE_CNAME_SUPPORT", "true")
provider := &fakeProvider{presentErr: errors.New("拒绝访问")}
tester := NewTester(fakeFactory{provider: provider})
_, err := tester.Verify(t.Context(), Cloudflare, "example.com", map[string]string{"api_token": "secret-token"})
if err == nil {
t.Fatal("创建测试记录失败时应返回错误")
}
if provider.cleaned {
t.Fatal("创建测试记录失败时不应执行清理")
}
}
func TestTesterCleansUpWithIndependentContextAfterQueryCancellation(t *testing.T) {
t.Setenv("LEGO_DISABLE_CNAME_SUPPORT", "true")
provider := &fakeProvider{}
tester := NewTesterWithResolver(fakeFactory{provider: provider}, missingTXTResolver{})
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := tester.Verify(ctx, Cloudflare, "example.com", map[string]string{"api_token": "secret-token"})
if err == nil {
t.Fatal("公网查询被取消时应返回错误")
}
if !provider.cleaned {
t.Fatal("公网查询被取消时仍应尝试清理测试记录")
}
if provider.cleanupSawCanceled {
t.Fatal("清理测试记录不应继承已取消的请求上下文")
}
}
type forbiddenTXTResolver struct{}
func (forbiddenTXTResolver) LookupTXT(context.Context, string) ([]string, error) {
return nil, errors.New("阿里云账号内权限验证不应查询公网 TXT 记录")
}
func TestTesterUsesAliyunAPIReadBackWithoutPublicDNSPropagation(t *testing.T) {
api := &authorizationRecordAPI{}
tester := &Tester{
resolver: forbiddenTXTResolver{},
aliyunAuthorization: newAuthorizationCheckerForTest(api),
}
report, err := tester.VerifyWithReport(t.Context(), Aliyun, "www.sub.example.com", scopeCredentials())
if err != nil {
t.Fatalf("阿里云权限验证失败:%v", err)
}
if report.RecordName == "" || len(report.Stages) != 3 {
t.Fatalf("阿里云阶段报告不完整:%+v", report)
}
for _, stage := range report.Stages {
if stage.Status != "completed" {
t.Fatalf("阿里云阶段未完成:%+v", report.Stages)
}
}
if report.Stages[0].Name != "创建记录" || report.Stages[1].Name != "查询记录" || report.Stages[2].Name != "清理记录" {
t.Fatalf("阿里云阶段顺序错误:%+v", report.Stages)
}
if !strings.Contains(report.Stages[1].Message, "未等待公网 DNS 传播") {
t.Fatalf("阿里云查询阶段必须明确不等待公网传播:%+v", report.Stages[1])
}
if got := callNames(api.calls); got != "add,describe,delete" {
t.Fatalf("阿里云不应调用公网 TXT 查询:%s", got)
}
}
func TestTesterReportsCleanupFailureAfterQueryFailure(t *testing.T) {
t.Setenv("LEGO_DISABLE_CNAME_SUPPORT", "true")
provider := &fakeProvider{cleanupErr: errors.New("权限不足")}
tester := NewTesterWithResolver(fakeFactory{provider: provider}, missingTXTResolver{})
ctx, cancel := context.WithCancel(context.Background())
cancel()
report, err := tester.Verify(ctx, Cloudflare, "example.com", map[string]string{"api_token": "secret-token"})
if err == nil || !strings.Contains(err.Error(), "自动清理失败") {
t.Fatalf("查询失败且清理失败时应返回组合诊断:%v", err)
}
if len(report.Stages) != 3 || report.Stages[1].Status != "failed" || report.Stages[2].Status != "failed" {
t.Fatalf("查询和清理失败阶段报告不完整:%+v", report.Stages)
}
}
func TestValidateCredentialsRejectsUnknownField(t *testing.T) {
_, err := ValidateCredentials(Cloudflare, map[string]string{"api_token": "token", "unexpected": "value"})
if err == nil {
t.Fatal("未知凭据字段应被拒绝")
}
}
func TestCNAMEProviderIsAvailable(t *testing.T) {
descriptor, err := GetDescriptor(CNAME)
if err != nil {
t.Fatalf("failed to read CNAME descriptor: %v", err)
}
if !descriptor.Available {
t.Fatal("CNAME delegation adapter should be available")
}
}
func TestLegoFactoryCreatesConfiguredProviders(t *testing.T) {
tests := []struct {
name string
providerID string
credentials map[string]string
}{
{
name: "阿里云 DNS",
providerID: Aliyun,
credentials: map[string]string{
"access_key_id": "id",
"access_key_secret": "secret",
},
},
{
name: "DNSPod",
providerID: DNSPod,
credentials: map[string]string{
"secret_id": "id",
"secret_key": "secret",
},
},
{
name: "DNSPod",
providerID: HuaweiCloud,
credentials: map[string]string{
"access_key_id": "id",
"secret_access_key": "secret",
"region": "invalid-region",
},
},
{
name: "华为云无效区域校验",
providerID: TencentCloud,
credentials: map[string]string{
"secret_id": "id",
"secret_key": "secret",
},
},
{
name: "AWS Route 53",
providerID: Route53,
credentials: map[string]string{
"access_key_id": "id",
"secret_access_key": "secret",
"region": "us-east-1",
},
},
}
factory := NewLegoFactory()
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
provider, err := factory.Create(test.providerID, test.credentials)
if test.providerID == HuaweiCloud {
if err == nil {
t.Fatal("无效的华为云区域应被拒绝")
}
if errors.Is(err, ErrProviderUnavailable) {
t.Fatalf("华为云已接入,不应返回未开放错误:%v", err)
}
return
}
if err != nil {
t.Fatalf("创建 Provider 失败:%v", err)
}
if provider == nil {
t.Fatal("创建 Provider 后返回空实例")
}
})
}
}
func TestGoogleCloudFactoryRejectsMalformedCredentials(t *testing.T) {
_, err := NewLegoFactory().Create(GoogleCloud, map[string]string{
"service_account_json": "{}",
"project": "test-project",
})
if err == nil {
t.Fatal("格式错误的 Google Cloud 服务账号凭据应被拒绝")
}
if errors.Is(err, ErrProviderUnavailable) {
t.Fatalf("Google Cloud 已接入,不应返回未开放错误:%v", err)
}
}