180 lines
5.3 KiB
Go
180 lines
5.3 KiB
Go
package usecase
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"lambdamail/protocols/internal/domain/entity"
|
|
)
|
|
|
|
func conflictTestInput() SyncDnsRecordsInput {
|
|
return SyncDnsRecordsInput{
|
|
DomainName: "example.test",
|
|
MailHost: "mail.example.test",
|
|
ServerIPv4: "192.0.2.1",
|
|
RsaDkimPubKey: "rsaKey",
|
|
EdDkimPubKey: "edKey",
|
|
}
|
|
}
|
|
|
|
// PLAN.md section 7.5: pre-existing records that are not ours are never
|
|
// touched, only reported as conflicts. A third-party verification TXT sits at
|
|
// the zone apex under the same type and name as our SPF record, so a reconciler
|
|
// keyed only on type+name would overwrite it and break the other service.
|
|
func TestSyncDnsRecords_ForeignApexTxtIsPreserved(t *testing.T) {
|
|
dns := newFakeDnsProvider()
|
|
dns.seed(entity.DnsRecord{
|
|
Type: "TXT",
|
|
Name: "example.test",
|
|
Value: "google-site-verification=abc123",
|
|
})
|
|
|
|
uc := NewSyncDnsRecordsUseCase(dns, nil)
|
|
out, err := uc.Execute(context.Background(), conflictTestInput())
|
|
if err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
|
|
apexTxt := dns.find("TXT", "example.test")
|
|
if len(apexTxt) != 2 {
|
|
t.Fatalf("expected the foreign TXT and our SPF to coexist, got %d records", len(apexTxt))
|
|
}
|
|
|
|
var foundForeign, foundSPF bool
|
|
for _, r := range apexTxt {
|
|
if r.Value == "google-site-verification=abc123" {
|
|
foundForeign = true
|
|
}
|
|
if strings.HasPrefix(r.Value, "v=spf1") {
|
|
foundSPF = true
|
|
}
|
|
}
|
|
if !foundForeign {
|
|
t.Error("the foreign verification TXT was destroyed")
|
|
}
|
|
if !foundSPF {
|
|
t.Error("the SPF record was not created alongside the foreign apex TXT")
|
|
}
|
|
if out.ConflictCount != 0 {
|
|
t.Errorf("a coexisting foreign TXT is not a conflict, got ConflictCount=%d", out.ConflictCount)
|
|
}
|
|
}
|
|
|
|
// A name that admits only one value (the mail host A record) already pointing
|
|
// somewhere else is a genuine conflict: silently repointing it could take down
|
|
// an unrelated service.
|
|
func TestSyncDnsRecords_ForeignARecordIsReportedAsConflict(t *testing.T) {
|
|
dns := newFakeDnsProvider()
|
|
dns.seed(entity.DnsRecord{
|
|
Type: "A",
|
|
Name: "mail.example.test",
|
|
Value: "198.51.100.9",
|
|
Comment: "managed by something else",
|
|
})
|
|
|
|
uc := NewSyncDnsRecordsUseCase(dns, nil)
|
|
out, err := uc.Execute(context.Background(), conflictTestInput())
|
|
if err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
|
|
records := dns.find("A", "mail.example.test")
|
|
if len(records) != 1 || records[0].Value != "198.51.100.9" {
|
|
t.Errorf("foreign A record was not left untouched: %+v", records)
|
|
}
|
|
if out.ConflictCount != 1 {
|
|
t.Fatalf("ConflictCount = %d, want 1", out.ConflictCount)
|
|
}
|
|
if out.Status != SyncStatusPartial {
|
|
t.Errorf("Status = %s, want %s", out.Status, SyncStatusPartial)
|
|
}
|
|
if !strings.Contains(out.Conflicts[0].String(), "198.51.100.9") {
|
|
t.Errorf("conflict report does not name the blocking value: %s", out.Conflicts[0])
|
|
}
|
|
}
|
|
|
|
// Our own records must still be updated in place when they drift.
|
|
func TestSyncDnsRecords_OwnRecordDriftIsCorrected(t *testing.T) {
|
|
dns := newFakeDnsProvider()
|
|
uc := NewSyncDnsRecordsUseCase(dns, nil)
|
|
ctx := context.Background()
|
|
|
|
if _, err := uc.Execute(ctx, conflictTestInput()); err != nil {
|
|
t.Fatalf("initial Execute: %v", err)
|
|
}
|
|
|
|
for i, r := range dns.records {
|
|
if r.Type == "A" && r.Name == "mail.example.test" {
|
|
dns.records[i].Value = "203.0.113.7"
|
|
}
|
|
}
|
|
|
|
out, err := uc.Execute(ctx, conflictTestInput())
|
|
if err != nil {
|
|
t.Fatalf("second Execute: %v", err)
|
|
}
|
|
|
|
if out.UpdatedCount != 1 || out.ConflictCount != 0 {
|
|
t.Errorf("updated=%d conflicts=%d, want updated=1 conflicts=0", out.UpdatedCount, out.ConflictCount)
|
|
}
|
|
if records := dns.find("A", "mail.example.test"); records[0].Value != "192.0.2.1" {
|
|
t.Errorf("drifted A record was not corrected: got %q", records[0].Value)
|
|
}
|
|
if out.Status != SyncStatusVerified {
|
|
t.Errorf("Status = %s, want %s", out.Status, SyncStatusVerified)
|
|
}
|
|
}
|
|
|
|
type stubDnsVerifier struct {
|
|
invisible map[string]bool
|
|
}
|
|
|
|
func (s stubDnsVerifier) VerifyRecord(_ context.Context, record entity.DnsRecord) (bool, string) {
|
|
if s.invisible[record.Name] {
|
|
return false, "no answer"
|
|
}
|
|
return true, ""
|
|
}
|
|
|
|
// PLAN.md section 7.5 verifies through public resolvers after writing. A
|
|
// record the provider accepted but nobody can resolve is drift, not success.
|
|
func TestSyncDnsRecords_UnresolvableRecordIsReportedAsDrift(t *testing.T) {
|
|
dns := newFakeDnsProvider()
|
|
uc := NewSyncDnsRecordsUseCase(dns, nil)
|
|
uc.SetVerifier(stubDnsVerifier{invisible: map[string]bool{"mail.example.test": true}})
|
|
|
|
out, err := uc.Execute(context.Background(), conflictTestInput())
|
|
if err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
|
|
if out.Status != SyncStatusDrift {
|
|
t.Errorf("Status = %s, want DRIFT", out.Status)
|
|
}
|
|
if len(out.Unverified) == 0 {
|
|
t.Fatal("the unresolvable record was not reported")
|
|
}
|
|
if !strings.Contains(out.Unverified[0], "mail.example.test") {
|
|
t.Errorf("Unverified = %v, want it to name the record", out.Unverified)
|
|
}
|
|
}
|
|
|
|
func TestSyncDnsRecords_AllRecordsVisibleIsVerified(t *testing.T) {
|
|
dns := newFakeDnsProvider()
|
|
uc := NewSyncDnsRecordsUseCase(dns, nil)
|
|
uc.SetVerifier(stubDnsVerifier{})
|
|
|
|
out, err := uc.Execute(context.Background(), conflictTestInput())
|
|
if err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
|
|
if out.Status != SyncStatusVerified {
|
|
t.Errorf("Status = %s, want VERIFIED", out.Status)
|
|
}
|
|
if len(out.Unverified) != 0 {
|
|
t.Errorf("Unverified = %v, want empty", out.Unverified)
|
|
}
|
|
}
|