Scert/backend/internal/dnsprovider/aliyun_scope_test.go

307 lines
11 KiB
Go
Raw Permalink 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"
)
func scopeCredentials() map[string]string {
return map[string]string{
"access_key_id": "test-id",
"access_key_secret": "test-secret",
}
}
func staticAliyunZoneResolver(zone string) AliyunZoneResolver {
return aliyunZoneResolverFunc(func(context.Context, string) (string, error) {
return zone, nil
})
}
func newAliyunAuthorizationCheckerForTest() *AliyunAuthorizationChecker {
checker := NewAliyunAuthorizationChecker()
checker.zoneResolver = staticAliyunZoneResolver("example.com")
return checker
}
func TestResolveAliyunTargetDomainUsesOnlyAuthoritativeZone(t *testing.T) {
resolverCalls := make([]string, 0, 1)
resolver := aliyunZoneResolverFunc(func(_ context.Context, requested string) (string, error) {
resolverCalls = append(resolverCalls, requested)
if requested != "www.sub.example.com" {
t.Fatalf("托管区识别收到了错误域名:%s", requested)
}
return "sub.example.com.", nil
})
got, err := resolveAliyunTargetDomain(t.Context(), "www.sub.example.com", resolver)
if err != nil || got != "sub.example.com" {
t.Fatalf("权威托管区识别结果错误:got=%q err=%v", got, err)
}
if len(resolverCalls) != 1 {
t.Fatalf("不应枚举候选父域:resolverCalls=%v", resolverCalls)
}
}
func TestAuthoritativeAliyunZoneResolverUsesFQDNAndBoundedDeadline(t *testing.T) {
const timeout = 80 * time.Millisecond
called := false
resolver := authoritativeAliyunZoneResolver{
timeout: timeout,
findZone: func(ctx context.Context, fqdn string) (string, error) {
called = true
if fqdn != "www.sub.example.com." {
t.Fatalf("权威托管区查询必须使用完整域名:%q", fqdn)
}
deadline, ok := ctx.Deadline()
if !ok {
t.Fatal("权威托管区查询必须具有截止时间")
}
remaining := time.Until(deadline)
if remaining <= 0 || remaining > timeout {
t.Fatalf("权威托管区查询截止时间未受限:%s", remaining)
}
return "sub.example.com.", nil
},
}
zone, err := resolver.Resolve(t.Context(), "WWW.Sub.Example.Com.")
if err != nil || zone != "sub.example.com" {
t.Fatalf("权威托管区解析结果错误:zone=%q err=%v", zone, err)
}
if !called {
t.Fatal("未执行权威托管区查询")
}
}
func TestResolveAliyunTargetDomainRejectsProtectedAndInvalidDomainsBeforeResolver(t *testing.T) {
resolverCalls := 0
resolver := aliyunZoneResolverFunc(func(context.Context, string) (string, error) {
resolverCalls++
return "example.com", nil
})
for _, name := range []string{"pcux.cn", "*.pcux.cn", "api.pcux.cn", "com", "*.com", ""} {
t.Run(name, func(t *testing.T) {
if _, err := resolveAliyunTargetDomain(t.Context(), name, resolver); err == nil {
t.Fatalf("resolveAliyunTargetDomain(%q) unexpectedly succeeded", name)
}
})
}
if resolverCalls != 0 {
t.Fatalf("受保护或无效域名不应发起 SOA 查询:%d", resolverCalls)
}
}
func TestResolveAliyunTargetDomainFailsClosedWhenAuthoritativeZoneCannotBeIdentified(t *testing.T) {
resolver := aliyunZoneResolverFunc(func(context.Context, string) (string, error) {
return "", errors.New("SOA lookup failed")
})
if _, err := resolveAliyunTargetDomain(t.Context(), "www.example.com", resolver); err == nil || !strings.Contains(err.Error(), "SOA lookup failed") {
t.Fatalf("权威托管区识别失败必须阻止后续操作:%v", err)
}
}
func TestResolveAliyunTargetDomainRejectsUnrelatedAuthoritativeZone(t *testing.T) {
resolver := staticAliyunZoneResolver("unrelated.example.net")
if _, err := resolveAliyunTargetDomain(t.Context(), "www.example.com", resolver); err == nil || !strings.Contains(err.Error(), "不属于") {
t.Fatalf("无关的权威托管区必须失败关闭:%v", err)
}
}
type fakeAliyunRecords struct {
zones, rrs, values, described, deleted []string
addErr, describeErr, deleteErr error
describeInfo aliyunRecordInfo
canceledCleanup bool
}
func (f *fakeAliyunRecords) Add(_ context.Context, zone, rr, value string) (string, error) {
f.zones = append(f.zones, zone)
f.rrs = append(f.rrs, rr)
f.values = append(f.values, value)
if f.addErr != nil {
return "", f.addErr
}
return "created-record-id", nil
}
func (f *fakeAliyunRecords) Describe(_ context.Context, zone, rr, id string) (aliyunRecordInfo, error) {
f.described = append(f.described, id)
if f.describeErr != nil {
return aliyunRecordInfo{}, f.describeErr
}
if f.describeInfo.RecordID != "" {
return f.describeInfo, nil
}
if len(f.zones) == 0 || len(f.rrs) == 0 || len(f.values) == 0 {
return aliyunRecordInfo{}, errors.New("no temporary record exists")
}
return aliyunRecordInfo{
RecordID: id,
DomainName: f.zones[len(f.zones)-1],
RR: f.rrs[len(f.rrs)-1],
Type: "TXT",
Value: f.values[len(f.values)-1],
}, nil
}
func (f *fakeAliyunRecords) Delete(ctx context.Context, id string) error {
f.deleted = append(f.deleted, id)
f.canceledCleanup = ctx.Err() != nil
return f.deleteErr
}
func TestAliyunProbeUsesTheSingleAuthoritativeZoneWithoutFallback(t *testing.T) {
api := &fakeAliyunRecords{addErr: errors.New("Forbidden.RAM")}
checker := newAliyunAuthorizationCheckerForTest()
checker.zoneResolver = staticAliyunZoneResolver("sub.example.com")
constructed := 0
checker.recordAPI = func(map[string]string) (aliyunRecordAPI, error) {
constructed++
return api, nil
}
if _, err := checker.Verify(t.Context(), "www.sub.example.com", scopeCredentials()); err == nil {
t.Fatal("RAM denial unexpectedly succeeded")
}
if constructed != 1 || len(api.zones) != 1 || api.zones[0] != "sub.example.com" {
t.Fatalf("探测未严格使用唯一权威托管区:constructed=%d zones=%v", constructed, api.zones)
}
if len(api.deleted) != 0 {
t.Fatalf("failed Add must not guess a record ID to delete: %v", api.deleted)
}
}
func TestAliyunProtectedDomainDoesNotConstructClientOrCallCloud(t *testing.T) {
api := &fakeAliyunRecords{}
checker := newAliyunAuthorizationCheckerForTest()
constructed := 0
checker.recordAPI = func(map[string]string) (aliyunRecordAPI, error) {
constructed++
return api, nil
}
for _, name := range []string{"pcux.cn", "*.pcux.cn", "api.pcux.cn"} {
if _, err := checker.Verify(t.Context(), name, scopeCredentials()); err == nil {
t.Fatalf("protected domain %q unexpectedly succeeded", name)
}
}
if constructed != 0 || len(api.zones) != 0 || len(api.described) != 0 || len(api.deleted) != 0 {
t.Fatalf("protected domain touched cloud state: constructed=%d zones=%v described=%v deleted=%v", constructed, api.zones, api.described, api.deleted)
}
}
func TestAliyunAuthorizationDoesNotConstructClientWhenZoneResolutionFails(t *testing.T) {
api := &fakeAliyunRecords{}
checker := newAliyunAuthorizationCheckerForTest()
checker.zoneResolver = aliyunZoneResolverFunc(func(context.Context, string) (string, error) {
return "", errors.New("SOA lookup failed")
})
constructed := 0
checker.recordAPI = func(map[string]string) (aliyunRecordAPI, error) {
constructed++
return api, nil
}
if _, err := checker.Verify(t.Context(), "www.example.com", scopeCredentials()); err == nil {
t.Fatal("权威托管区识别失败时不应继续探测")
}
if constructed != 0 || len(api.zones) != 0 || len(api.described) != 0 || len(api.deleted) != 0 {
t.Fatalf("权威托管区识别失败时触碰了云端:constructed=%d zones=%v described=%v deleted=%v", constructed, api.zones, api.described, api.deleted)
}
}
func TestAliyunProviderDefersClientCreationAndPinsCleanupIdentity(t *testing.T) {
provider, err := newScopedAliyunProvider(scopeCredentials())
if err != nil {
t.Fatal(err)
}
api := &fakeAliyunRecords{}
constructed := 0
provider.newAPI = func(map[string]string) (aliyunRecordAPI, error) {
constructed++
return api, nil
}
provider.zoneResolver = staticAliyunZoneResolver("sub.example.com")
provider.challengeInfo = func(_ context.Context, name, _ string) (string, string, string) {
fqdn := "_acme-challenge." + name + "."
return fqdn, fqdn, "txt-value"
}
if constructed != 0 {
t.Fatal("provider construction must not construct a cloud client")
}
if err := provider.Present(t.Context(), "www.sub.example.com", "token", "key"); err != nil {
t.Fatal(err)
}
if constructed != 1 || len(api.zones) != 1 || api.zones[0] != "sub.example.com" || api.rrs[0] != "_acme-challenge.www" {
t.Fatalf("provider 写入位置不属于唯一权威托管区:constructed=%d zones=%v rrs=%v", constructed, api.zones, api.rrs)
}
if err := provider.Present(t.Context(), "www.sub.example.com", "token", "key"); err == nil {
t.Fatal("duplicate challenge unexpectedly overwrote a pending record")
}
if err := provider.CleanUp(t.Context(), "www.sub.example.com", "other-token", "key"); err != nil {
t.Fatal(err)
}
if len(api.deleted) != 0 {
t.Fatalf("cleanup for a different challenge deleted a record: %v", api.deleted)
}
ctx, cancel := context.WithCancel(t.Context())
cancel()
if err := provider.CleanUp(ctx, "www.sub.example.com", "token", "key"); err != nil {
t.Fatal(err)
}
if len(api.deleted) != 1 || api.deleted[0] != "created-record-id" || api.canceledCleanup {
t.Fatalf("cleanup did not use the exact created record ID: deleted=%v canceled=%v", api.deleted, api.canceledCleanup)
}
}
func TestAliyunProviderRejectsCNAMEBeforeClientConstruction(t *testing.T) {
provider, err := newScopedAliyunProvider(scopeCredentials())
if err != nil {
t.Fatal(err)
}
constructed := 0
provider.newAPI = func(map[string]string) (aliyunRecordAPI, error) {
constructed++
return &fakeAliyunRecords{}, nil
}
provider.zoneResolver = staticAliyunZoneResolver("example.com")
provider.challengeInfo = func(context.Context, string, string) (string, string, string) {
return "_acme-challenge.www.sub.example.com.", "_acme-challenge.delegated.example.net.", "txt-value"
}
if err := provider.Present(t.Context(), "www.sub.example.com", "token", "key"); err == nil || !strings.Contains(err.Error(), "CNAME") {
t.Fatalf("CNAME redirection was not rejected: %v", err)
}
if constructed != 0 {
t.Fatalf("CNAME redirection constructed a cloud client: %d", constructed)
}
}
func TestAliyunProviderDoesNotConstructClientWhenZoneResolutionFails(t *testing.T) {
provider, err := newScopedAliyunProvider(scopeCredentials())
if err != nil {
t.Fatal(err)
}
provider.zoneResolver = aliyunZoneResolverFunc(func(context.Context, string) (string, error) {
return "", errors.New("SOA lookup failed")
})
constructed := 0
provider.newAPI = func(map[string]string) (aliyunRecordAPI, error) {
constructed++
return &fakeAliyunRecords{}, nil
}
if err := provider.Present(t.Context(), "www.example.com", "token", "key"); err == nil {
t.Fatal("权威托管区识别失败时不应继续写入")
}
if constructed != 0 {
t.Fatalf("权威托管区识别失败时不应构造阿里云客户端:%d", constructed)
}
}