671 lines
25 KiB
Go
671 lines
25 KiB
Go
package setup
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"strings"
|
||
"sync"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
type fakeDNSTester struct {
|
||
err error
|
||
}
|
||
|
||
func TestDNSVerificationStoresOnlyCurrentAliyunCredentials(t *testing.T) {
|
||
store := &memorySecretWriter{}
|
||
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
|
||
input := DNSVerificationInput{ProviderID: "aliyun", TestDomain: "www.example.com", Credentials: map[string]string{
|
||
"access_key_id": "test-id", "access_key_secret": "test-secret",
|
||
"managed_zones": "example.com", "allowed_domains": "www.example.com",
|
||
}}
|
||
first, err := manager.Verify(t.Context(), input)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, ok := store.values[first.CredentialRef]["managed_zones"]; ok {
|
||
t.Fatal("legacy managed zone must not be persisted as a credential")
|
||
}
|
||
if _, ok := store.values[first.CredentialRef]["allowed_domains"]; ok {
|
||
t.Fatal("legacy allowed domain must not be persisted as a credential")
|
||
}
|
||
input.Credentials["access_key_secret"] = "new-secret"
|
||
second, err := manager.Verify(t.Context(), input)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if first.CredentialRef == second.CredentialRef || store.values[first.CredentialRef]["access_key_secret"] != "test-secret" {
|
||
t.Fatal("credential changes must not overwrite an already verified reference")
|
||
}
|
||
if err := manager.Check(first.VerificationToken, "aliyun", second.CredentialRef, input.TestDomain); err == nil {
|
||
t.Fatal("old token must not authorize a new credential reference")
|
||
}
|
||
}
|
||
|
||
func (f fakeDNSTester) Verify(context.Context, string, string, map[string]string) (DNSVerificationReport, error) {
|
||
return DNSVerificationReport{}, f.err
|
||
}
|
||
|
||
type memorySecretWriter struct {
|
||
mu sync.Mutex
|
||
values map[string]map[string]string
|
||
deleted []string
|
||
putErr error
|
||
deleteErr error
|
||
}
|
||
|
||
type cancellingSecretWriter struct {
|
||
memorySecretWriter
|
||
cancel context.CancelFunc
|
||
}
|
||
|
||
type retryableSecretWriter struct {
|
||
memorySecretWriter
|
||
failDelete bool
|
||
}
|
||
|
||
type cancellingRetryableSecretWriter struct {
|
||
retryableSecretWriter
|
||
cancel context.CancelFunc
|
||
}
|
||
|
||
func (s *cancellingSecretWriter) Put(ctx context.Context, reference string, value map[string]string) error {
|
||
if err := s.memorySecretWriter.Put(ctx, reference, value); err != nil {
|
||
return err
|
||
}
|
||
s.cancel()
|
||
return nil
|
||
}
|
||
|
||
func (s *retryableSecretWriter) Delete(ctx context.Context, reference string) error {
|
||
if s.failDelete {
|
||
return errors.New("delete failed")
|
||
}
|
||
return s.memorySecretWriter.Delete(ctx, reference)
|
||
}
|
||
|
||
func (s *cancellingRetryableSecretWriter) Put(ctx context.Context, reference string, value map[string]string) error {
|
||
if err := s.retryableSecretWriter.Put(ctx, reference, value); err != nil {
|
||
return err
|
||
}
|
||
s.cancel()
|
||
return nil
|
||
}
|
||
|
||
func (m *memorySecretWriter) Put(_ context.Context, reference string, value map[string]string) error {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
if m.values == nil {
|
||
m.values = map[string]map[string]string{}
|
||
}
|
||
copy := make(map[string]string, len(value))
|
||
for key, item := range value {
|
||
copy[key] = item
|
||
}
|
||
m.values[reference] = copy
|
||
return m.putErr
|
||
}
|
||
|
||
func (m *memorySecretWriter) Delete(_ context.Context, reference string) error {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
m.deleted = append(m.deleted, reference)
|
||
delete(m.values, reference)
|
||
return m.deleteErr
|
||
}
|
||
|
||
func TestDNSVerificationManagerIssuesOneTimeToken(t *testing.T) {
|
||
store := &memorySecretWriter{}
|
||
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
|
||
result, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||
ProviderID: "cloudflare", TestDomain: "example.com",
|
||
Credentials: map[string]string{"api_token": "secret"},
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("DNS 在线验证失败:%v", err)
|
||
}
|
||
if result.CredentialRef == "" || result.VerificationToken == "" {
|
||
t.Fatalf("DNS 验证结果不完整:%+v", result)
|
||
}
|
||
if len(store.values) != 1 {
|
||
t.Fatalf("DNS 凭据没有写入敏感信息存储:%v", store.values)
|
||
}
|
||
if err := manager.Check(result.VerificationToken, "cloudflare", result.CredentialRef, "example.com"); err != nil {
|
||
t.Fatalf("检查 DNS 验证令牌失败:%v", err)
|
||
}
|
||
if err := manager.Check(result.VerificationToken, "cloudflare", result.CredentialRef, "example.com"); err != nil {
|
||
t.Fatalf("检查 DNS 验证令牌不应消费令牌:%v", err)
|
||
}
|
||
if err := manager.Consume(result.VerificationToken, "cloudflare", result.CredentialRef, "example.com"); err != nil {
|
||
t.Fatalf("消费 DNS 验证令牌失败:%v", err)
|
||
}
|
||
if err := manager.Check(result.VerificationToken, "cloudflare", result.CredentialRef, "example.com"); err == nil {
|
||
t.Fatal("已消费的 DNS 验证令牌不应继续通过检查")
|
||
}
|
||
if err := manager.Consume(result.VerificationToken, "cloudflare", result.CredentialRef, "example.com"); err == nil {
|
||
t.Fatal("DNS 验证令牌只能使用一次")
|
||
}
|
||
}
|
||
|
||
func TestDNSVerificationManagerRejectsMismatchedConfiguration(t *testing.T) {
|
||
manager := NewDNSVerificationManager(fakeDNSTester{}, &memorySecretWriter{})
|
||
result, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||
ProviderID: "cloudflare", TestDomain: "example.com",
|
||
Credentials: map[string]string{"api_token": "secret"},
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("DNS 在线验证失败:%v", err)
|
||
}
|
||
if err := manager.Consume(result.VerificationToken, "cloudflare", result.CredentialRef, "other.example.com"); err == nil {
|
||
t.Fatal("DNS 验证结果不应允许绑定到其他域名")
|
||
}
|
||
}
|
||
|
||
func TestDNSVerificationDiscardOnlyRemovesSelectedToken(t *testing.T) {
|
||
store := &memorySecretWriter{}
|
||
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
|
||
first, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||
ProviderID: "aliyun", TestDomain: "first.example.com",
|
||
Credentials: map[string]string{"access_key_id": "first-id", "access_key_secret": "first-secret"},
|
||
})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
second, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||
ProviderID: "aliyun", TestDomain: "second.example.com",
|
||
Credentials: map[string]string{"access_key_id": "second-id", "access_key_secret": "second-secret"},
|
||
})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := manager.Discard(first.VerificationToken); err != nil {
|
||
t.Fatalf("精确回收 DNS 验证令牌失败:%v", err)
|
||
}
|
||
if err := manager.Check(first.VerificationToken, "aliyun", first.CredentialRef, "first.example.com"); err == nil {
|
||
t.Fatal("已回收的 DNS 验证令牌仍然有效")
|
||
}
|
||
if err := manager.Check(second.VerificationToken, "aliyun", second.CredentialRef, "second.example.com"); err != nil {
|
||
t.Fatalf("精确回收影响了其他 DNS 验证令牌:%v", err)
|
||
}
|
||
store.mu.Lock()
|
||
_, firstExists := store.values[first.CredentialRef]
|
||
_, secondExists := store.values[second.CredentialRef]
|
||
store.mu.Unlock()
|
||
if firstExists || !secondExists {
|
||
t.Fatalf("DNS 凭据回收范围错误:first=%v second=%v", firstExists, secondExists)
|
||
}
|
||
if err := manager.Discard(first.VerificationToken); err == nil {
|
||
t.Fatal("DNS 验证令牌被重复回收")
|
||
}
|
||
}
|
||
|
||
func TestDNSVerificationDiscardAllRemovesPendingCredentials(t *testing.T) {
|
||
store := &memorySecretWriter{}
|
||
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
|
||
results := make([]DNSVerificationResult, 0, 2)
|
||
for _, testDomain := range []string{"first.example.com", "second.example.com"} {
|
||
result, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||
ProviderID: "aliyun", TestDomain: testDomain,
|
||
Credentials: map[string]string{"access_key_id": "test-id", "access_key_secret": "test-secret"},
|
||
})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
results = append(results, result)
|
||
}
|
||
manager.DiscardAll()
|
||
for _, result := range results {
|
||
if err := manager.Check(result.VerificationToken, "aliyun", result.CredentialRef, "first.example.com"); err == nil {
|
||
t.Fatal("DiscardAll 后 DNS 验证令牌仍然有效")
|
||
}
|
||
}
|
||
store.mu.Lock()
|
||
remaining := len(store.values)
|
||
store.mu.Unlock()
|
||
if remaining != 0 {
|
||
t.Fatalf("DiscardAll 后仍有 DNS 临时凭据:%d", remaining)
|
||
}
|
||
}
|
||
|
||
func TestDNSVerificationDiscardAllExceptProtectsFormalCredential(t *testing.T) {
|
||
store := &memorySecretWriter{}
|
||
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
|
||
protected, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||
ProviderID: "aliyun", TestDomain: "formal.example.com",
|
||
Credentials: map[string]string{"access_key_id": "formal-id", "access_key_secret": "formal-secret"},
|
||
})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
pending, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||
ProviderID: "aliyun", TestDomain: "pending.example.com",
|
||
Credentials: map[string]string{"access_key_id": "pending-id", "access_key_secret": "pending-secret"},
|
||
})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
manager.DiscardAllExcept(" " + protected.CredentialRef + " ")
|
||
|
||
if _, err := manager.Resolve(protected.VerificationToken); err == nil {
|
||
t.Fatal("批量清理后受保护凭据对应的验证令牌仍然有效")
|
||
}
|
||
if _, err := manager.Resolve(pending.VerificationToken); err == nil {
|
||
t.Fatal("批量清理后待处理凭据对应的验证令牌仍然有效")
|
||
}
|
||
store.mu.Lock()
|
||
_, protectedExists := store.values[protected.CredentialRef]
|
||
_, pendingExists := store.values[pending.CredentialRef]
|
||
store.mu.Unlock()
|
||
if !protectedExists || pendingExists {
|
||
t.Fatalf("DNS 凭据保护范围错误:protected=%v pending=%v", protectedExists, pendingExists)
|
||
}
|
||
}
|
||
|
||
func TestDNSVerificationManagerRejectsExpiredToken(t *testing.T) {
|
||
store := &memorySecretWriter{}
|
||
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
|
||
now := time.Date(2026, 8, 26, 8, 0, 0, 0, time.UTC)
|
||
manager.now = func() time.Time { return now }
|
||
result, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||
ProviderID: "cloudflare", TestDomain: "example.com",
|
||
Credentials: map[string]string{"api_token": "secret"},
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("DNS 在线验证失败:%v", err)
|
||
}
|
||
now = now.Add(dnsVerificationLifetime + time.Second)
|
||
if err := manager.Consume(result.VerificationToken, "cloudflare", result.CredentialRef, "example.com"); err == nil {
|
||
t.Fatal("过期 DNS 验证令牌应被拒绝")
|
||
}
|
||
if len(store.deleted) != 1 || store.deleted[0] != result.CredentialRef {
|
||
t.Fatalf("expired session did not delete temporary credential: %v", store.deleted)
|
||
}
|
||
}
|
||
|
||
func TestDNSVerificationManagerDeletesCredentialWhenPutReportsFailure(t *testing.T) {
|
||
store := &memorySecretWriter{putErr: errors.New("write failed")}
|
||
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
|
||
_, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||
ProviderID: "cloudflare", TestDomain: "example.com",
|
||
Credentials: map[string]string{"api_token": "secret"},
|
||
})
|
||
if err == nil {
|
||
t.Fatal("Put failure unexpectedly succeeded")
|
||
}
|
||
if len(store.deleted) != 1 {
|
||
t.Fatalf("Put failure did not trigger credential cleanup: %v", store.deleted)
|
||
}
|
||
if len(store.values) != 0 {
|
||
t.Fatalf("Put failure left temporary credentials stored: %v", store.values)
|
||
}
|
||
}
|
||
|
||
func TestDNSVerificationManagerRetriesCleanupAfterPutFailure(t *testing.T) {
|
||
store := &retryableSecretWriter{failDelete: true}
|
||
store.putErr = errors.New("write failed")
|
||
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
|
||
_, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||
ProviderID: "cloudflare", TestDomain: "example.com",
|
||
Credentials: map[string]string{"api_token": "secret"},
|
||
})
|
||
if err == nil {
|
||
t.Fatal("保存失败时应返回错误")
|
||
}
|
||
if len(store.values) != 1 || len(manager.pendingCleanup) != 1 {
|
||
t.Fatalf("删除失败后应保留待清理凭据:values=%d pending=%d", len(store.values), len(manager.pendingCleanup))
|
||
}
|
||
|
||
store.putErr = nil
|
||
store.failDelete = false
|
||
if _, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||
ProviderID: "cloudflare", TestDomain: "example.com",
|
||
Credentials: map[string]string{"api_token": "next-secret"},
|
||
}); err != nil {
|
||
t.Fatalf("下一次验证应先重试待清理凭据:%v", err)
|
||
}
|
||
if len(manager.pendingCleanup) != 0 || len(store.values) != 1 {
|
||
t.Fatalf("待清理凭据重试后状态错误:pending=%d values=%d", len(manager.pendingCleanup), len(store.values))
|
||
}
|
||
}
|
||
|
||
func TestDNSVerificationManagerRetriesCleanupAfterCancellation(t *testing.T) {
|
||
ctx, cancel := context.WithCancel(t.Context())
|
||
store := &cancellingRetryableSecretWriter{
|
||
retryableSecretWriter: retryableSecretWriter{failDelete: true},
|
||
cancel: cancel,
|
||
}
|
||
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
|
||
|
||
_, err := manager.Verify(ctx, DNSVerificationInput{
|
||
ProviderID: "cloudflare", TestDomain: "example.com",
|
||
Credentials: map[string]string{"api_token": "secret"},
|
||
})
|
||
if !errors.Is(err, context.Canceled) {
|
||
t.Fatalf("保存后取消应返回 context.Canceled:%v", err)
|
||
}
|
||
if len(store.values) != 1 || len(manager.pendingCleanup) != 1 {
|
||
t.Fatalf("取消后的删除失败应进入待清理状态:values=%d pending=%d", len(store.values), len(manager.pendingCleanup))
|
||
}
|
||
|
||
store.failDelete = false
|
||
if _, err := manager.Resolve("missing-token"); err == nil {
|
||
t.Fatal("不存在的令牌应返回错误")
|
||
}
|
||
if len(store.values) != 0 || len(manager.pendingCleanup) != 0 {
|
||
t.Fatalf("取消后的凭据重试未完成:values=%d pending=%d", len(store.values), len(manager.pendingCleanup))
|
||
}
|
||
}
|
||
|
||
func TestDNSVerificationManagerRejectsAlreadyCancelledContextBeforeTesting(t *testing.T) {
|
||
tester := &countingDNSTester{}
|
||
store := &memorySecretWriter{}
|
||
manager := NewDNSVerificationManager(tester, store)
|
||
ctx, cancel := context.WithCancel(t.Context())
|
||
cancel()
|
||
|
||
_, err := manager.Verify(ctx, DNSVerificationInput{
|
||
ProviderID: "cloudflare", TestDomain: "example.com",
|
||
Credentials: map[string]string{"api_token": "secret"},
|
||
})
|
||
if !errors.Is(err, context.Canceled) {
|
||
t.Fatalf("已取消上下文应返回 context.Canceled:%v", err)
|
||
}
|
||
if tester.CallCount() != 0 || len(store.values) != 0 {
|
||
t.Fatalf("已取消上下文不应执行测试或保存凭据:calls=%d values=%v", tester.CallCount(), store.values)
|
||
}
|
||
}
|
||
|
||
func TestDNSVerificationManagerCleansCredentialWhenContextCancelsAfterStore(t *testing.T) {
|
||
ctx, cancel := context.WithCancel(t.Context())
|
||
store := &cancellingSecretWriter{cancel: cancel}
|
||
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
|
||
|
||
result, err := manager.Verify(ctx, DNSVerificationInput{
|
||
ProviderID: "cloudflare", TestDomain: "example.com",
|
||
Credentials: map[string]string{"api_token": "secret"},
|
||
})
|
||
if !errors.Is(err, context.Canceled) {
|
||
t.Fatalf("保存凭据后取消应返回 context.Canceled:%v", err)
|
||
}
|
||
if result.CredentialRef != "" || result.VerificationToken != "" {
|
||
t.Fatalf("取消的 DNS 验证不应发布令牌:%+v", result)
|
||
}
|
||
if len(store.values) != 0 {
|
||
t.Fatalf("保存后取消未回收临时凭据:%v", store.values)
|
||
}
|
||
if len(store.deleted) != 1 {
|
||
t.Fatalf("保存后取消未执行临时凭据回收:%v", store.deleted)
|
||
}
|
||
}
|
||
|
||
func TestDNSVerificationManagerRetainsTokenWhenDiscardCleanupFails(t *testing.T) {
|
||
store := &retryableSecretWriter{failDelete: true}
|
||
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
|
||
result, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||
ProviderID: "cloudflare", TestDomain: "example.com",
|
||
Credentials: map[string]string{"api_token": "secret"},
|
||
})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := manager.Discard(result.VerificationToken); err == nil {
|
||
t.Fatal("临时凭据回收失败时应返回错误")
|
||
}
|
||
if _, err := manager.Resolve(result.VerificationToken); err != nil {
|
||
t.Fatalf("回收失败后应保留验证令牌以便重试:%v", err)
|
||
}
|
||
if len(store.values) != 1 {
|
||
t.Fatalf("回收失败后不应丢失临时凭据:%v", store.values)
|
||
}
|
||
|
||
store.failDelete = false
|
||
if err := manager.Discard(result.VerificationToken); err != nil {
|
||
t.Fatalf("临时凭据回收重试失败:%v", err)
|
||
}
|
||
if len(store.values) != 0 {
|
||
t.Fatalf("回收重试后仍有临时凭据:%v", store.values)
|
||
}
|
||
}
|
||
|
||
func TestDNSVerificationManagerRetriesDiscardAllCleanupFailures(t *testing.T) {
|
||
store := &retryableSecretWriter{failDelete: true}
|
||
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
|
||
for range 2 {
|
||
if _, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||
ProviderID: "cloudflare", TestDomain: "example.com",
|
||
Credentials: map[string]string{"api_token": "secret"},
|
||
}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
|
||
manager.DiscardAll()
|
||
if len(manager.sessions) != 2 || len(store.values) != 2 {
|
||
t.Fatalf("批量回收失败后应保留可重试会话:sessions=%d values=%d", len(manager.sessions), len(store.values))
|
||
}
|
||
store.failDelete = false
|
||
manager.DiscardAll()
|
||
if len(manager.sessions) != 0 || len(store.values) != 0 {
|
||
t.Fatalf("批量回收重试后仍有临时状态:sessions=%d values=%d", len(manager.sessions), len(store.values))
|
||
}
|
||
}
|
||
|
||
type countingDNSTester struct {
|
||
mu sync.Mutex
|
||
calls int
|
||
block <-chan struct{}
|
||
}
|
||
|
||
func (f *countingDNSTester) Verify(_ context.Context, _ string, _ string, _ map[string]string) (DNSVerificationReport, error) {
|
||
f.mu.Lock()
|
||
f.calls++
|
||
f.mu.Unlock()
|
||
if f.block != nil {
|
||
<-f.block
|
||
}
|
||
return DNSVerificationReport{}, nil
|
||
}
|
||
|
||
func (f *countingDNSTester) CallCount() int {
|
||
f.mu.Lock()
|
||
defer f.mu.Unlock()
|
||
return f.calls
|
||
}
|
||
|
||
func TestDNSVerificationManagerRejectsCapacityBeforeTestingOrStoring(t *testing.T) {
|
||
tester := &countingDNSTester{}
|
||
store := &memorySecretWriter{}
|
||
manager := NewDNSVerificationManager(tester, store)
|
||
manager.sessions = make(map[string]dnsVerificationSession, dnsVerificationSessionCapacity)
|
||
expiresAt := time.Now().UTC().Add(time.Hour)
|
||
for index := 0; index < dnsVerificationSessionCapacity; index++ {
|
||
manager.sessions[string(rune(index))] = dnsVerificationSession{CredentialRef: "secret://existing", ExpiresAt: expiresAt}
|
||
}
|
||
_, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||
ProviderID: "cloudflare", TestDomain: "example.com", Credentials: map[string]string{"api_token": "secret"},
|
||
})
|
||
if err == nil || tester.CallCount() != 0 || len(store.values) != 0 {
|
||
t.Fatalf("capacity rejection must precede testing and storage: err=%v calls=%d values=%v", err, tester.CallCount(), store.values)
|
||
}
|
||
}
|
||
|
||
func TestDNSVerificationManagerCountsExpiredCleanupAgainstCapacity(t *testing.T) {
|
||
tester := &countingDNSTester{}
|
||
store := &memorySecretWriter{}
|
||
manager := NewDNSVerificationManager(tester, store)
|
||
manager.sessions = make(map[string]dnsVerificationSession, dnsVerificationSessionCapacity-1)
|
||
manager.expiredSession = make(map[string]expiredDNSVerificationSession, 1)
|
||
expiresAt := time.Now().UTC().Add(time.Hour)
|
||
for index := 0; index < dnsVerificationSessionCapacity-1; index++ {
|
||
manager.sessions[string(rune(index))] = dnsVerificationSession{CredentialRef: "secret://existing", ExpiresAt: expiresAt}
|
||
}
|
||
manager.expiredSession["secret://pending"] = expiredDNSVerificationSession{
|
||
Token: "pending-token",
|
||
Session: dnsVerificationSession{CredentialRef: "secret://pending", ExpiresAt: expiresAt},
|
||
}
|
||
|
||
_, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||
ProviderID: "cloudflare", TestDomain: "example.com", Credentials: map[string]string{"api_token": "secret"},
|
||
})
|
||
if err == nil || tester.CallCount() != 0 || len(store.values) != 0 {
|
||
t.Fatalf("待清理凭据应计入容量限制:err=%v calls=%d values=%v", err, tester.CallCount(), store.values)
|
||
}
|
||
}
|
||
|
||
func TestDNSVerificationManagerRejectsInFlightCapacityBeforeTestingOrStoring(t *testing.T) {
|
||
tester := &countingDNSTester{}
|
||
store := &memorySecretWriter{}
|
||
manager := NewDNSVerificationManager(tester, store)
|
||
manager.inFlight = dnsVerificationInFlightCapacity
|
||
_, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||
ProviderID: "cloudflare", TestDomain: "example.com", Credentials: map[string]string{"api_token": "secret"},
|
||
})
|
||
if err == nil || tester.CallCount() != 0 || len(store.values) != 0 {
|
||
t.Fatalf("in-flight rejection must precede testing and storage: err=%v calls=%d values=%v", err, tester.CallCount(), store.values)
|
||
}
|
||
}
|
||
|
||
func TestDNSVerificationManagerConsumesTokenOnlyOnceUnderConcurrency(t *testing.T) {
|
||
manager := NewDNSVerificationManager(fakeDNSTester{}, &memorySecretWriter{})
|
||
result, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||
ProviderID: "cloudflare", TestDomain: "example.com", Credentials: map[string]string{"api_token": "secret"},
|
||
})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
start := make(chan struct{})
|
||
results := make(chan error, 2)
|
||
for range 2 {
|
||
go func() {
|
||
<-start
|
||
results <- manager.Consume(result.VerificationToken, "cloudflare", result.CredentialRef, "example.com")
|
||
}()
|
||
}
|
||
close(start)
|
||
successes := 0
|
||
for range 2 {
|
||
if <-results == nil {
|
||
successes++
|
||
}
|
||
}
|
||
if successes != 1 {
|
||
t.Fatalf("concurrent Consume successes = %d; want 1", successes)
|
||
}
|
||
}
|
||
|
||
func TestDNSVerificationManagerDoesNotStoreFailedCredential(t *testing.T) {
|
||
store := &memorySecretWriter{}
|
||
manager := NewDNSVerificationManager(fakeDNSTester{err: errors.New("权限不足")}, store)
|
||
_, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||
ProviderID: "cloudflare", TestDomain: "example.com",
|
||
Credentials: map[string]string{"api_token": "secret"},
|
||
})
|
||
if err == nil {
|
||
t.Fatal("DNS 测试失败时应返回错误")
|
||
}
|
||
if len(store.values) != 0 {
|
||
t.Fatal("DNS 测试失败时不应保存凭据")
|
||
}
|
||
}
|
||
|
||
type recordingDNSTester struct {
|
||
credentials map[string]string
|
||
}
|
||
|
||
func (f *recordingDNSTester) Verify(_ context.Context, _ string, _ string, credentials map[string]string) (DNSVerificationReport, error) {
|
||
f.credentials = credentials
|
||
return DNSVerificationReport{}, nil
|
||
}
|
||
|
||
func TestDNSVerificationManagerNormalizesCredentialCopyBeforeTestingAndStoring(t *testing.T) {
|
||
store := &memorySecretWriter{}
|
||
tester := &recordingDNSTester{}
|
||
manager := NewDNSVerificationManager(tester, store)
|
||
inputCredentials := map[string]string{" api_token ": " secret-token "}
|
||
result, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||
ProviderID: "cloudflare", TestDomain: "example.com", Credentials: inputCredentials,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("DNS 在线验证失败:%v", err)
|
||
}
|
||
if tester.credentials["api_token"] != "secret-token" {
|
||
t.Fatalf("传给测试器的凭据未规范化:%v", tester.credentials)
|
||
}
|
||
if _, ok := tester.credentials[" api_token "]; ok {
|
||
t.Fatal("测试器不应收到带首尾空格的凭据键")
|
||
}
|
||
stored := store.values[result.CredentialRef]
|
||
if stored["api_token"] != "secret-token" {
|
||
t.Fatalf("保存的凭据未规范化:%v", stored)
|
||
}
|
||
if inputCredentials[" api_token "] != " secret-token " {
|
||
t.Fatal("规范化过程不应修改调用方的原始凭据 Map")
|
||
}
|
||
}
|
||
|
||
func TestDNSVerificationResolveIsServerOnlyAndDoesNotConsumeToken(t *testing.T) {
|
||
store := &memorySecretWriter{}
|
||
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
|
||
result, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||
ProviderID: "aliyun", TestDomain: "www.example.com",
|
||
Credentials: map[string]string{"access_key_id": "test-id", "access_key_secret": "test-secret"},
|
||
})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
resolved, err := manager.Resolve(result.VerificationToken)
|
||
if err != nil {
|
||
t.Fatalf("按令牌解析 DNS 验证结果失败:%v", err)
|
||
}
|
||
if resolved.ProviderID != "aliyun" || resolved.TestDomain != "www.example.com" ||
|
||
resolved.CredentialRef != result.CredentialRef || resolved.ExpiresAt != result.ExpiresAt {
|
||
t.Fatalf("解析的 DNS 验证状态不完整:%+v", resolved)
|
||
}
|
||
encoded, err := json.Marshal(resolved)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if strings.Contains(string(encoded), result.CredentialRef) || strings.Contains(string(encoded), resolved.TestDomain) {
|
||
t.Fatalf("服务端 DNS 验证状态可被意外序列化:%s", encoded)
|
||
}
|
||
if err := manager.Consume(result.VerificationToken, resolved.ProviderID, resolved.CredentialRef, resolved.TestDomain); err != nil {
|
||
t.Fatalf("Resolve 不应消费 DNS 验证令牌:%v", err)
|
||
}
|
||
if _, err := manager.Resolve(result.VerificationToken); err == nil {
|
||
t.Fatal("已消费的 DNS 验证令牌仍可被解析")
|
||
}
|
||
}
|
||
|
||
func TestDNSVerificationManagerRetriesExpiredCredentialCleanup(t *testing.T) {
|
||
store := &retryableSecretWriter{failDelete: true}
|
||
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
|
||
now := time.Date(2026, 9, 30, 12, 0, 0, 0, time.UTC)
|
||
manager.now = func() time.Time { return now }
|
||
result, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||
ProviderID: "cloudflare", TestDomain: "example.com",
|
||
Credentials: map[string]string{"api_token": "secret"},
|
||
})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
now = now.Add(dnsVerificationLifetime + time.Second)
|
||
if err := manager.Check(result.VerificationToken, "cloudflare", result.CredentialRef, "example.com"); err == nil {
|
||
t.Fatal("expired DNS verification token should be rejected")
|
||
}
|
||
if len(manager.sessions) != 1 || len(store.values) != 1 {
|
||
t.Fatalf("failed expired cleanup should remain retryable: sessions=%d values=%d", len(manager.sessions), len(store.values))
|
||
}
|
||
|
||
store.failDelete = false
|
||
if err := manager.Check(result.VerificationToken, "cloudflare", result.CredentialRef, "example.com"); err == nil {
|
||
t.Fatal("expired DNS verification token should remain rejected after cleanup retry")
|
||
}
|
||
if len(manager.sessions) != 0 || len(store.values) != 0 {
|
||
t.Fatalf("expired cleanup retry left temporary state: sessions=%d values=%d", len(manager.sessions), len(store.values))
|
||
}
|
||
}
|