#10: implement Cloudflare DNS automation engine (13 records & system aliases)

This commit is contained in:
Lucas Rayan committed 2026-07-25 05:26:14 -03:00
1 parent b08bce71bb
commit 10836ea98f
14 files changed
+1043 -2

No files matched your search

@@ -0,0 +1,16 @@
package port
import (
"context"
"lambdamail/protocols/internal/domain/entity"
)
// DnsProvider defines operations for managing remote DNS zone records (e.g. Cloudflare).
type DnsProvider interface {
GetZoneID(ctx context.Context, domainName string) (string, error)
ListRecords(ctx context.Context, zoneID string) ([]entity.DnsRecord, error)
CreateRecord(ctx context.Context, zoneID string, record entity.DnsRecord) error
UpdateRecord(ctx context.Context, zoneID string, record entity.DnsRecord) error
DeleteRecord(ctx context.Context, zoneID string, recordID string) error
}
@@ -0,0 +1,8 @@
package port
import "context"
// SystemAliasRepository manages mandatory system security aliases (postmaster, abuse, dmarc, tlsrpt).
type SystemAliasRepository interface {
EnsureSystemAliases(ctx context.Context, domainName string, adminEmail string) error
}
@@ -0,0 +1,104 @@
package usecase
import (
"context"
"fmt"
"strings"
"lambdamail/protocols/internal/application/port"
"lambdamail/protocols/internal/domain/entity"
)
type SyncDnsRecordsInput struct {
DomainName string
MailHost string
ServerIPv4 string
ServerIPv6 string
RsaDkimPubKey string
EdDkimPubKey string
TlsaHash string
DaneEnabled bool
AdminEmailAddress string
}
type SyncDnsRecordsOutput struct {
ZoneID string
CreatedCount int
UpdatedCount int
UnchangedCount int
}
type SyncDnsRecordsUseCase struct {
dnsProvider port.DnsProvider
aliasRepo port.SystemAliasRepository
}
func NewSyncDnsRecordsUseCase(dnsProvider port.DnsProvider, aliasRepo port.SystemAliasRepository) *SyncDnsRecordsUseCase {
return &SyncDnsRecordsUseCase{
dnsProvider: dnsProvider,
aliasRepo: aliasRepo,
}
}
func (uc *SyncDnsRecordsUseCase) Execute(ctx context.Context, input SyncDnsRecordsInput) (*SyncDnsRecordsOutput, error) {
if input.DomainName == "" {
return nil, fmt.Errorf("domain name is required")
}
desiredSpecs := entity.Build13DnsRecordSpecs(
input.DomainName,
input.MailHost,
input.ServerIPv4,
input.ServerIPv6,
input.RsaDkimPubKey,
input.EdDkimPubKey,
input.TlsaHash,
input.DaneEnabled,
)
zoneID, err := uc.dnsProvider.GetZoneID(ctx, input.DomainName)
if err != nil {
return nil, fmt.Errorf("get zone id for %s: %w", input.DomainName, err)
}
existingRecords, err := uc.dnsProvider.ListRecords(ctx, zoneID)
if err != nil {
return nil, fmt.Errorf("list dns records for zone %s: %w", zoneID, err)
}
existingMap := make(map[string]entity.DnsRecord)
for _, rec := range existingRecords {
key := fmt.Sprintf("%s:%s", strings.ToUpper(rec.Type), strings.ToLower(rec.Name))
existingMap[key] = rec
}
output := &SyncDnsRecordsOutput{ZoneID: zoneID}
for _, spec := range desiredSpecs {
key := fmt.Sprintf("%s:%s", strings.ToUpper(spec.Type), strings.ToLower(spec.Name))
existing, found := existingMap[key]
if !found {
if err := uc.dnsProvider.CreateRecord(ctx, zoneID, spec); err != nil {
return nil, fmt.Errorf("create dns record %s %s: %w", spec.Type, spec.Name, err)
}
output.CreatedCount++
} else if !spec.EqualsNormalized(existing) {
spec.ID = existing.ID
if err := uc.dnsProvider.UpdateRecord(ctx, zoneID, spec); err != nil {
return nil, fmt.Errorf("update dns record %s %s: %w", spec.Type, spec.Name, err)
}
output.UpdatedCount++
} else {
output.UnchangedCount++
}
}
if input.AdminEmailAddress != "" && uc.aliasRepo != nil {
if err := uc.aliasRepo.EnsureSystemAliases(ctx, input.DomainName, input.AdminEmailAddress); err != nil {
return nil, fmt.Errorf("ensure system aliases: %w", err)
}
}
return output, nil
}
@@ -0,0 +1,104 @@
package usecase
import (
"context"
"fmt"
"strings"
"testing"
"lambdamail/protocols/internal/domain/entity"
)
type fakeDnsProvider struct {
records map[string]entity.DnsRecord // key -> record
}
func newFakeDnsProvider() *fakeDnsProvider {
return &fakeDnsProvider{records: make(map[string]entity.DnsRecord)}
}
func (f *fakeDnsProvider) GetZoneID(_ context.Context, domainName string) (string, error) {
return fmt.Sprintf("zone_%s", domainName), nil
}
func (f *fakeDnsProvider) ListRecords(_ context.Context, _ string) ([]entity.DnsRecord, error) {
var out []entity.DnsRecord
for _, r := range f.records {
out = append(out, r)
}
return out, nil
}
func (f *fakeDnsProvider) CreateRecord(_ context.Context, _ string, record entity.DnsRecord) error {
key := fmt.Sprintf("%s:%s", strings.ToUpper(record.Type), strings.ToLower(record.Name))
record.ID = fmt.Sprintf("id_%s", key)
f.records[key] = record
return nil
}
func (f *fakeDnsProvider) UpdateRecord(_ context.Context, _ string, record entity.DnsRecord) error {
key := fmt.Sprintf("%s:%s", strings.ToUpper(record.Type), strings.ToLower(record.Name))
f.records[key] = record
return nil
}
func (f *fakeDnsProvider) DeleteRecord(_ context.Context, _ string, recordID string) error {
for k, r := range f.records {
if r.ID == recordID {
delete(f.records, k)
break
}
}
return nil
}
type fakeSystemAliasRepository struct {
ensured bool
}
func (f *fakeSystemAliasRepository) EnsureSystemAliases(_ context.Context, _ string, _ string) error {
f.ensured = true
return nil
}
func TestSyncDnsRecordsUseCase_Creates13RecordsAndAliases(t *testing.T) {
dns := newFakeDnsProvider()
alias := &fakeSystemAliasRepository{}
uc := NewSyncDnsRecordsUseCase(dns, alias)
ctx := context.Background()
input := SyncDnsRecordsInput{
DomainName: "example.test",
MailHost: "mail.example.test",
ServerIPv4: "192.0.2.1",
ServerIPv6: "2001:db8::1",
RsaDkimPubKey: "rsaKey",
EdDkimPubKey: "edKey",
TlsaHash: "hash123",
DaneEnabled: true,
AdminEmailAddress: "admin@example.test",
}
// 1. Initial Sync -> Creates 13 records
out, err := uc.Execute(ctx, input)
if err != nil {
t.Fatalf("Execute failed: %v", err)
}
if out.CreatedCount != 13 {
t.Errorf("CreatedCount = %d, want 13", out.CreatedCount)
}
if !alias.ensured {
t.Error("expected system aliases to be ensured")
}
// 2. Second Sync (idempotent) -> Unchanged 13 records
out2, err := uc.Execute(ctx, input)
if err != nil {
t.Fatalf("Execute 2 failed: %v", err)
}
if out2.UnchangedCount != 13 || out2.CreatedCount != 0 || out2.UpdatedCount != 0 {
t.Errorf("expected 13 unchanged, got created=%d, updated=%d, unchanged=%d", out2.CreatedCount, out2.UpdatedCount, out2.UnchangedCount)
}
}