diff --git a/internal/mikrotik/record.go b/internal/mikrotik/record.go index 9f34dd9..e4604bf 100644 --- a/internal/mikrotik/record.go +++ b/internal/mikrotik/record.go @@ -47,60 +47,63 @@ type DNSRecord struct { func NewDNSRecord(endpoint *endpoint.Endpoint) (*DNSRecord, error) { log.Debugf("Converting ExternalDNS endpoint to MikrotikDNS: %v", endpoint) + // Sanity checks -> Fields are not empty and if set, they are set correctly if endpoint.DNSName == "" { return nil, fmt.Errorf("DNS name is required") } - if endpoint.RecordType == "" { return nil, fmt.Errorf("record type is required") } - if len(endpoint.Targets) == 0 || endpoint.Targets[0] == "" { return nil, fmt.Errorf("no target provided for DNS record") } - record := &DNSRecord{Name: endpoint.DNSName} - log.Debugf("Name set to: %s", record.Name) + // Convert ExternalDNS TTL to Mikrotik TTL + ttl, err := endpointTTLtoMikrotikTTL(endpoint.RecordTTL) + if err != nil { + return nil, fmt.Errorf("failed to convert TTL: %v", err) + } - record.Type = endpoint.RecordType + // Initialize new records + record := &DNSRecord{Name: endpoint.DNSName, Type: endpoint.RecordType, TTL: ttl} + log.Debugf("Name set to: %s", record.Name) log.Debugf("Type set to: %s", record.Type) + log.Debugf("TTL set to: %s", record.TTL) + // Record-type specific data switch record.Type { case "A": - record.Address = endpoint.Targets[0] - if net.ParseIP(record.Address) == nil || strings.Contains(record.Address, ":") { - return nil, fmt.Errorf("invalid IPv4 address: %s", record.Address) + if err := validateIPv4(endpoint.Targets[0]); err != nil { + return nil, err } + record.Address = endpoint.Targets[0] log.Debugf("Address set to: %s", record.Address) + case "AAAA": - record.Address = endpoint.Targets[0] - if net.ParseIP(record.Address) == nil || !strings.Contains(record.Address, ":") { - return nil, fmt.Errorf("invalid IPv6 address: %s", record.Address) + if err := validateIPv6(endpoint.Targets[0]); err != nil { + return nil, err } + record.Address = endpoint.Targets[0] log.Debugf("Address set to: %s", record.Address) + case "CNAME": - record.CName = endpoint.Targets[0] - if record.CName == "" { - return nil, fmt.Errorf("CNAME target cannot be empty") + if err := validateDomain(endpoint.Targets[0]); err != nil { + return nil, err } - log.Debugf("CName set to: %s", record.CName) + record.CName = endpoint.Targets[0] + log.Debugf("CNAME set to: %s", record.Address) + case "TXT": - record.Text = endpoint.Targets[0] - if record.Text == "" { - return nil, fmt.Errorf("TXT record text cannot be empty") + if err := validateTXT(endpoint.Targets[0]); err != nil { + return nil, err } + record.Text = endpoint.Targets[0] log.Debugf("Text set to: %s", record.Text) + default: return nil, fmt.Errorf("unsupported DNS type: %s", endpoint.RecordType) } - ttl, err := endpointTTLtoMikrotikTTL(endpoint.RecordTTL) - if err != nil { - return nil, fmt.Errorf("failed to convert TTL: %v", err) - } - record.TTL = ttl - log.Debugf("TTL set to: %s", record.TTL) - for _, providerSpecific := range endpoint.ProviderSpecific { switch providerSpecific.Name { case "comment": @@ -132,36 +135,75 @@ func NewDNSRecord(endpoint *endpoint.Endpoint) (*DNSRecord, error) { func (r *DNSRecord) toExternalDNSEndpoint() (*endpoint.Endpoint, error) { log.Debugf("Converting MikrotikDNS record to ExternalDNS: %v", r) + // ============================================================================================ + // Sanity checks + // ============================================================================================ + if r.Name == "" { + return nil, fmt.Errorf("DNS record name cannot be empty") + } + + //? Mikrotik assumes A-records are default and sometimes omits setting the type if r.Type == "" { log.Debugf("Record type not set. Using default value 'A'") r.Type = "A" } + ttl, err := mikrotikTTLtoEndpointTTL(r.TTL) + if err != nil { + return nil, fmt.Errorf("failed to convert MikrotikDNS record to ExternalDNS: %v", err) + } + + // Initialize endpoint ep := endpoint.Endpoint{ DNSName: r.Name, RecordType: r.Type, + RecordTTL: ttl, } + // ============================================================================================ + // Record-specific data + // ============================================================================================ switch ep.RecordType { - case "A", "AAAA": + case "A": + if err := validateIPv4(r.Address); err != nil { + return nil, err + } + ep.Targets = endpoint.NewTargets(r.Address) + log.Debugf("Address set to: %s", r.Address) + + case "AAAA": + if err := validateIPv6(r.Address); err != nil { + return nil, err + } ep.Targets = endpoint.NewTargets(r.Address) + log.Debugf("Address set to: %s", r.Address) + case "CNAME": + if err := validateDomain(r.CName); err != nil { + return nil, err + } ep.Targets = endpoint.NewTargets(r.CName) + log.Debugf("CNAME set to: %s", r.CName) + case "TXT": + if err := validateTXT(r.Text); err != nil { + return nil, err + } ep.Targets = endpoint.NewTargets(r.Text) + log.Debugf("Text set to: %s", r.Text) + default: return nil, fmt.Errorf("unsupported DNS type: %s", ep.RecordType) } + + // Ensure at least one target is present and non-empty if len(ep.Targets) == 0 || ep.Targets[0] == "" { return nil, fmt.Errorf("no target provided for DNS record") } - ttl, err := mikrotikTTLtoEndpointTTL(r.TTL) - if err != nil { - return nil, fmt.Errorf("failed to convert MikrotikDNS record to ExternalDNS: %v", err) - } - ep.RecordTTL = ttl - + // ============================================================================================ + // Provider-specific stuff + // ============================================================================================ if r.Comment != "" { ep.ProviderSpecific = append(ep.ProviderSpecific, endpoint.ProviderSpecificProperty{ Name: "comment", @@ -191,6 +233,9 @@ func (r *DNSRecord) toExternalDNSEndpoint() (*endpoint.Endpoint, error) { return &ep, nil } +// ================================================================================================ +// UTILS +// ================================================================================================ // mikrotikTTLtoEndpointTTL converts a Mikrotik TTL to an ExternalDNS TTL func mikrotikTTLtoEndpointTTL(ttl string) (endpoint.TTL, error) { log.Debugf("Converting Mikrotik TTL to Endpoint TTL: %s", ttl) @@ -296,3 +341,57 @@ func endpointTTLtoMikrotikTTL(ttl endpoint.TTL) (string, error) { log.Debugf("Converted TTL: %v", durationStr) return durationStr, nil } + +// validateIPv4 checks if the provided address is a valid IPv4 address. +func validateIPv4(address string) error { + if net.ParseIP(address) == nil { + return fmt.Errorf("invalid IP address: %s", address) + } + + if strings.Contains(address, ":") { + return fmt.Errorf("provided address looks like an IPv6 address: %s", address) + } + + return nil +} + +// validateIPv6 checks if the provided address is a valid IPv6 address. +func validateIPv6(address string) error { + if net.ParseIP(address) == nil { + return fmt.Errorf("invalid IP address: %s", address) + } + + if !strings.Contains(address, ":") { + return fmt.Errorf("provided address looks like an IPv4 address: %s", address) + } + + return nil +} + +// validateTXT checks if the provided TXT record text is valid. +func validateTXT(text string) error { + if text == "" { + return fmt.Errorf("TXT record text cannot be empty") + } + //? TODO: add more validation here? + return nil +} + +// validateDomain checks if the provided domain is semantically valid. +func validateDomain(domain string) error { + if domain == "" { + return fmt.Errorf("a domain cannot be empty") + } + + if len(domain) > 253 { + return fmt.Errorf("invalid domain, length exceeds 253 characters") + } + + domainRegex := `^(?i:[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?\.)+[a-z]{2,}$` + matched, err := regexp.MatchString(domainRegex, domain) + if err != nil || !matched { + return fmt.Errorf("invalid domain: %s", domain) + } + + return nil +} diff --git a/internal/mikrotik/record_test.go b/internal/mikrotik/record_test.go index 973452b..a5b2393 100644 --- a/internal/mikrotik/record_test.go +++ b/internal/mikrotik/record_test.go @@ -1,12 +1,107 @@ package mikrotik import ( + "strings" "testing" "github.com/stretchr/testify/assert" "sigs.k8s.io/external-dns/endpoint" ) +// ================================================================================================ +// Test Validation Functions +// ================================================================================================ +func TestValidateIPv4(t *testing.T) { + tests := []struct { + name string + address string + expectError bool + }{ + {"Valid IPv4 address", "192.168.1.1", false}, + {"Invalid IPv4 address", "256.256.256.256", true}, + {"Looks like IPv6", "2001:0db8:85a3:0000:0000:8a2e:0370:7334", true}, + {"Empty address", "", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateIPv4(tt.address) + if (err != nil) != tt.expectError { + t.Errorf("expected error: %v, got: %v for address: %s", tt.expectError, err, tt.address) + } + }) + } +} + +func TestValidateIPv6(t *testing.T) { + tests := []struct { + name string + address string + expectError bool + }{ + {"Valid IPv6 address", "2001:0db8:85a3:0000:0000:8a2e:0370:7334", false}, + {"Invalid IPv6 address", "1200:0000:AB00:1234:0000:2552:7777:1313:3", true}, + {"Looks like IPv4", "192.168.1.1", true}, + {"Empty address", "", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateIPv6(tt.address) + if (err != nil) != tt.expectError { + t.Errorf("expected error: %v, got: %v for address: %s", tt.expectError, err, tt.address) + } + }) + } +} + +func TestValidateTXT(t *testing.T) { + tests := []struct { + name string + text string + expectError bool + }{ + {"Valid TXT record", "This is a valid TXT record", false}, + {"Empty TXT record", "", true}, + {"Single space TXT record", " ", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateTXT(tt.text) + if (err != nil) != tt.expectError { + t.Errorf("expected error: %v, got: %v for TXT record: %s", tt.expectError, err, tt.text) + } + }) + } +} + +func TestValidateDomain(t *testing.T) { + tests := []struct { + name string + domain string + expectError bool + }{ + {"Valid domain", "example.com", false}, + {"Invalid domain with underscores", "example_domain.com", true}, + {"Too long domain", strings.Repeat("a", 255) + ".com", true}, + {"Empty domain", "", true}, + {"Invalid domain format", "invalid_domain", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateDomain(tt.domain) + if (err != nil) != tt.expectError { + t.Errorf("expected error: %v, got: %v for domain: %s", tt.expectError, err, tt.domain) + } + }) + } +} + +// ================================================================================================ +// Test TTL Conversion Functions +// ================================================================================================ func TestMikrotikTTLtoEndpointTTL(t *testing.T) { tests := []struct { name string @@ -71,6 +166,9 @@ func TestEndpointTTLtoMikrotikTTL(t *testing.T) { } } +// ================================================================================================ +// Test DNS Record Conversion Functions +// ================================================================================================ func TestDNSRecordToExternalDNSEndpoint(t *testing.T) { tests := []struct { name string @@ -78,6 +176,9 @@ func TestDNSRecordToExternalDNSEndpoint(t *testing.T) { expected *endpoint.Endpoint expectError bool }{ + // =============================================================== + // A RECORD TEST CASES + // =============================================================== { name: "Valid A record", record: &DNSRecord{ @@ -94,6 +195,43 @@ func TestDNSRecordToExternalDNSEndpoint(t *testing.T) { }, expectError: false, }, + { + name: "Invalid A record (empty address)", + record: &DNSRecord{ + Name: "invalid.example.com", + Type: "A", + Address: "", + TTL: "1h", + }, + expected: nil, + expectError: true, + }, + { + name: "Invalid A record (malformed address)", + record: &DNSRecord{ + Name: "invalid.example.com", + Type: "A", + Address: "999.999.999.999", + TTL: "1h", + }, + expected: nil, + expectError: true, + }, + { + name: "Invalid A record (IPv6 address)", + record: &DNSRecord{ + Name: "invalid.example.com", + Type: "A", + Address: "2001:db8::1", + TTL: "1h", + }, + expected: nil, + expectError: true, + }, + + // =============================================================== + // AAAA RECORD TEST CASES + // =============================================================== { name: "Valid AAAA record", record: &DNSRecord{ @@ -110,6 +248,43 @@ func TestDNSRecordToExternalDNSEndpoint(t *testing.T) { }, expectError: false, }, + { + name: "Invalid AAAA record (empty address)", + record: &DNSRecord{ + Name: "invalid.example.com", + Type: "AAAA", + Address: "", + TTL: "1h", + }, + expected: nil, + expectError: true, + }, + { + name: "Invalid AAAA record (IPv4 address)", + record: &DNSRecord{ + Name: "invalid.example.com", + Type: "AAAA", + Address: "1.2.3.4", + TTL: "1h", + }, + expected: nil, + expectError: true, + }, + { + name: "Invalid AAAA record (malformed address)", + record: &DNSRecord{ + Name: "invalid.example.com", + Type: "AAAA", + Address: "1200:0000:AB00:1234:0000:2552:7777:1313:3:31", + TTL: "1h", + }, + expected: nil, + expectError: true, + }, + + // =============================================================== + // CNAME RECORD TEST CASES + // =============================================================== { name: "Valid CNAME record", record: &DNSRecord{ @@ -126,6 +301,32 @@ func TestDNSRecordToExternalDNSEndpoint(t *testing.T) { }, expectError: false, }, + { + name: "Invalid CNAME record (empty cname)", + record: &DNSRecord{ + Name: "invalid.example.com", + Type: "CNAME", + CName: "", + TTL: "30m", + }, + expected: nil, + expectError: true, + }, + { + name: "Invalid CNAME record (malformed domain)", + record: &DNSRecord{ + Name: "invalid.example.com", + Type: "CNAME", + CName: "sub......domain...here-", + TTL: "30m", + }, + expected: nil, + expectError: true, + }, + + // =============================================================== + // TXT RECORD TEST CASES + // =============================================================== { name: "Valid TXT record", record: &DNSRecord{ @@ -142,70 +343,23 @@ func TestDNSRecordToExternalDNSEndpoint(t *testing.T) { }, expectError: false, }, - - { - name: "Record with match-subdomain", - record: &DNSRecord{ - Name: "example.com", - Type: "CNAME", - CName: "example.org", - TTL: "30m", - MatchSubdomain: "yes", - }, - expected: &endpoint.Endpoint{ - DNSName: "example.com", - RecordType: "CNAME", - Targets: endpoint.NewTargets("example.org"), - RecordTTL: endpoint.TTL(1800), - ProviderSpecific: endpoint.ProviderSpecific{ - {Name: "match-subdomain", Value: "yes"}, - }, - }, - expectError: false, - }, - { - name: "Record with address-list", - record: &DNSRecord{ - Name: "blocked.example.com", - Type: "A", - Address: "192.0.2.123", - TTL: "1h", - AddressList: "blocked", - }, - expected: &endpoint.Endpoint{ - DNSName: "blocked.example.com", - RecordType: "A", - Targets: endpoint.NewTargets("192.0.2.123"), - RecordTTL: endpoint.TTL(3600), - ProviderSpecific: endpoint.ProviderSpecific{ - {Name: "address-list", Value: "blocked"}, - }, - }, - expectError: false, - }, - { - name: "Invalid TTL in DNSRecord", - record: &DNSRecord{ - Name: "example.com", - Type: "A", - Address: "192.0.2.1", - TTL: "invalid", - }, - expected: nil, - expectError: true, - }, { - name: "Unsupported record type", + name: "Invalid TXT record (empty text)", record: &DNSRecord{ - Name: "example.com", - Type: "MX", - TTL: "1h", + Name: "invalid.example.com", + Type: "TXT", + Text: "", + TTL: "10m", }, expected: nil, expectError: true, }, + + // =============================================================== + // PROVIDER-SPECIFIC DATA TEST CASES + // =============================================================== { - name: "Provider-specific properties", + name: "Valid Provider-specific properties", record: &DNSRecord{ Name: "example.com", Type: "TXT", @@ -230,10 +384,16 @@ func TestDNSRecordToExternalDNSEndpoint(t *testing.T) { }, expectError: false, }, + // TODO: invalid provider specific + + // =============================================================== + // DEFAULT VALUES FOR UNSET FIELDS TEST CASES + // =============================================================== { name: "Empty Type (should default to 'A')", record: &DNSRecord{ - Name: "example.com", + Name: "example.com", + //! Type is empty Address: "192.0.2.1", TTL: "1h", }, @@ -251,7 +411,7 @@ func TestDNSRecordToExternalDNSEndpoint(t *testing.T) { Name: "example.com", Type: "A", Address: "192.0.2.1", - // TTL is empty + //! TTL is empty }, expected: &endpoint.Endpoint{ DNSName: "example.com", @@ -261,46 +421,27 @@ func TestDNSRecordToExternalDNSEndpoint(t *testing.T) { }, expectError: false, }, + + // =============================================================== + // GENERIC ERROR CASES + // =============================================================== { - name: "Invalid A record (empty address)", + name: "Invalid TTL in DNSRecord", record: &DNSRecord{ - Name: "invalid.example.com", + Name: "example.com", Type: "A", - Address: "", - TTL: "1h", - }, - expected: nil, - expectError: true, - }, - { - name: "Invalid AAAA record (empty address)", - record: &DNSRecord{ - Name: "invalid.example.com", - Type: "AAAA", - Address: "", - TTL: "1h", - }, - expected: nil, - expectError: true, - }, - { - name: "Invalid CNAME record (empty cname)", - record: &DNSRecord{ - Name: "invalid.example.com", - Type: "CNAME", - CName: "", - TTL: "30m", + Address: "192.0.2.1", + TTL: "invalid", }, expected: nil, expectError: true, }, { - name: "Invalid TXT record (empty text)", + name: "Unsupported record type", record: &DNSRecord{ - Name: "invalid.example.com", - Type: "TXT", - Text: "", - TTL: "10m", + Name: "example.com", + Type: "FWD", + TTL: "1h", }, expected: nil, expectError: true, @@ -311,7 +452,7 @@ func TestDNSRecordToExternalDNSEndpoint(t *testing.T) { Name: "empty.example.com", Type: "A", TTL: "1h", - // Address is empty + //! Address is empty }, expected: nil, expectError: true, @@ -356,7 +497,9 @@ func TestExternalDNSEndpointToDNSRecord(t *testing.T) { expected *DNSRecord expectError bool }{ - // Valid A record + // =============================================================== + // A RECORD TEST CASES + // =============================================================== { name: "Valid A record", endpoint: &endpoint.Endpoint{ @@ -373,116 +516,104 @@ func TestExternalDNSEndpointToDNSRecord(t *testing.T) { }, expectError: false, }, - // Valid AAAA record { - name: "Valid AAAA record", + name: "Valid A record with multiple targets", endpoint: &endpoint.Endpoint{ - DNSName: "ipv6.example.com", - RecordType: "AAAA", - Targets: endpoint.NewTargets("2001:db8::1"), - RecordTTL: endpoint.TTL(7200), + DNSName: "multi.example.com", + RecordType: "A", + Targets: endpoint.NewTargets("192.0.2.1", "192.0.2.2"), + RecordTTL: endpoint.TTL(3600), }, expected: &DNSRecord{ - Name: "ipv6.example.com", - Type: "AAAA", - Address: "2001:db8::1", - TTL: "2h", + Name: "multi.example.com", + Type: "A", + Address: "192.0.2.1", // Should use the first target + TTL: "1h", }, expectError: false, }, - // Valid CNAME record { - name: "Valid CNAME record", + name: "Invalid A record (emopty address)", endpoint: &endpoint.Endpoint{ - DNSName: "www.example.com", - RecordType: "CNAME", - Targets: endpoint.NewTargets("example.com"), - RecordTTL: endpoint.TTL(1800), - }, - expected: &DNSRecord{ - Name: "www.example.com", - Type: "CNAME", - CName: "example.com", - TTL: "30m", + DNSName: "invalid.example.com", + RecordType: "A", + Targets: endpoint.NewTargets(), + RecordTTL: endpoint.TTL(3600), }, - expectError: false, + expected: nil, + expectError: true, }, - // Valid TXT record { - name: "Valid TXT record", + name: "Invalid A record (malformed address)", endpoint: &endpoint.Endpoint{ - DNSName: "example.com", - RecordType: "TXT", - Targets: endpoint.NewTargets("v=spf1 include:example.com ~all"), - RecordTTL: endpoint.TTL(600), - }, - expected: &DNSRecord{ - Name: "example.com", - Type: "TXT", - Text: "v=spf1 include:example.com ~all", - TTL: "10m", + DNSName: "invalid.example.com", + RecordType: "A", + Targets: endpoint.NewTargets("999.999.999.999"), + RecordTTL: endpoint.TTL(3600), }, - expectError: false, + expected: nil, + expectError: true, }, - // Valid A record with multiple targets (should use first target) { - name: "Valid A record with multiple targets", + name: "Invalid A record (IPv6 address)", endpoint: &endpoint.Endpoint{ - DNSName: "multi.example.com", + DNSName: "invalid.example.com", RecordType: "A", - Targets: endpoint.NewTargets("192.0.2.1", "192.0.2.2"), + Targets: endpoint.NewTargets("2001:db8::1"), RecordTTL: endpoint.TTL(3600), }, + expected: nil, + expectError: true, + }, + + // =============================================================== + // AAAA RECORD TEST CASES + // =============================================================== + { + name: "Valid AAAA record", + endpoint: &endpoint.Endpoint{ + DNSName: "ipv6.example.com", + RecordType: "AAAA", + Targets: endpoint.NewTargets("2001:db8::1"), + RecordTTL: endpoint.TTL(7200), + }, expected: &DNSRecord{ - Name: "multi.example.com", - Type: "A", - Address: "192.0.2.1", // Should use the first target - TTL: "1h", + Name: "ipv6.example.com", + Type: "AAAA", + Address: "2001:db8::1", + TTL: "2h", }, expectError: false, }, - // Valid record with provider-specific properties { - name: "Valid record with provider-specific properties", + name: "Valid AAAA record with multiple targets", endpoint: &endpoint.Endpoint{ - DNSName: "provider.example.com", - RecordType: "A", - Targets: endpoint.NewTargets("192.0.2.3"), + DNSName: "multi.example.com", + RecordType: "AAAA", + Targets: endpoint.NewTargets("2001:db8::1", "2001:db8::2"), RecordTTL: endpoint.TTL(3600), - ProviderSpecific: endpoint.ProviderSpecific{ - {Name: "comment", Value: "Test comment"}, - {Name: "regexp", Value: "^www\\."}, - {Name: "match-subdomain", Value: "yes"}, - {Name: "address-list", Value: "list1"}, - }, }, expected: &DNSRecord{ - Name: "provider.example.com", - Type: "A", - Address: "192.0.2.3", - TTL: "1h", - Comment: "Test comment", - Regexp: "^www\\.", - MatchSubdomain: "yes", - AddressList: "list1", + Name: "multi.example.com", + Type: "AAAA", + Address: "2001:db8::1", // Should use the first target + TTL: "1h", }, expectError: false, }, - // Invalid A record (invalid IP address) { - name: "Invalid A record (invalid IP address)", + name: "Invalid AAAA record (empty address)", endpoint: &endpoint.Endpoint{ - DNSName: "invalid.example.com", - RecordType: "A", - Targets: endpoint.NewTargets("999.999.999.999"), + DNSName: "multi.example.com", + RecordType: "AAAA", + Targets: endpoint.NewTargets(""), RecordTTL: endpoint.TTL(3600), }, expected: nil, expectError: true, }, - // Invalid AAAA record (invalid IPv6 address) { - name: "Invalid AAAA record (invalid IPv6 address)", + name: "Invalid AAAA record (malformed address)", endpoint: &endpoint.Endpoint{ DNSName: "invalid.example.com", RecordType: "AAAA", @@ -492,122 +623,107 @@ func TestExternalDNSEndpointToDNSRecord(t *testing.T) { expected: nil, expectError: true, }, - // Invalid CNAME record (empty target) { - name: "Invalid CNAME record (empty target)", + name: "Invalid AAAA record (IPv4 address)", endpoint: &endpoint.Endpoint{ DNSName: "invalid.example.com", - RecordType: "CNAME", - Targets: endpoint.NewTargets(""), - RecordTTL: endpoint.TTL(1800), + RecordType: "AAAA", + Targets: endpoint.NewTargets("1.2.3.4"), + RecordTTL: endpoint.TTL(3600), }, expected: nil, expectError: true, }, - // Invalid TXT record (empty text) + + // =============================================================== + // CNAME RECORD TEST CASES + // =============================================================== { - name: "Invalid TXT record (empty text)", + name: "Valid CNAME record", endpoint: &endpoint.Endpoint{ - DNSName: "invalid.example.com", - RecordType: "TXT", - Targets: endpoint.NewTargets(""), - RecordTTL: endpoint.TTL(600), + DNSName: "www.example.com", + RecordType: "CNAME", + Targets: endpoint.NewTargets("example.com"), + RecordTTL: endpoint.TTL(1800), }, - expected: nil, - expectError: true, + expected: &DNSRecord{ + Name: "www.example.com", + Type: "CNAME", + CName: "example.com", + TTL: "30m", + }, + expectError: false, }, - // Record with empty targets { - name: "Record with empty targets", + name: "Invalid CNAME record (empty target)", endpoint: &endpoint.Endpoint{ - DNSName: "empty.example.com", - RecordType: "A", - Targets: endpoint.Targets{}, - RecordTTL: endpoint.TTL(3600), + DNSName: "invalid.example.com", + RecordType: "CNAME", + Targets: endpoint.NewTargets(""), + RecordTTL: endpoint.TTL(1800), }, expected: nil, expectError: true, }, - // Unsupported record type { - name: "Unsupported record type", + name: "Invalid CNAME record (malformed target)", endpoint: &endpoint.Endpoint{ - DNSName: "unsupported.example.com", - RecordType: "MX", - Targets: endpoint.NewTargets("mail.example.com"), - RecordTTL: endpoint.TTL(3600), + DNSName: "invalid.example.com", + RecordType: "CNAME", + Targets: endpoint.NewTargets("sub...............domain"), + RecordTTL: endpoint.TTL(1800), }, expected: nil, expectError: true, }, - // Invalid provider-specific configuration + + // =============================================================== + // TXT RECORD TEST CASES + // =============================================================== { - name: "Invalid provider-specific configuration", + name: "Valid TXT record", endpoint: &endpoint.Endpoint{ DNSName: "example.com", RecordType: "TXT", - Targets: endpoint.NewTargets("some text"), - ProviderSpecific: endpoint.ProviderSpecific{ - {Name: "unsupported", Value: "value"}, - }, + Targets: endpoint.NewTargets("v=spf1 include:example.com ~all"), + RecordTTL: endpoint.TTL(600), }, - expected: nil, - expectError: true, - }, - // Empty DNSName - { - name: "Empty DNSName", - endpoint: &endpoint.Endpoint{ - DNSName: "", - RecordType: "A", - Targets: endpoint.NewTargets("192.0.2.1"), - RecordTTL: endpoint.TTL(3600), + expected: &DNSRecord{ + Name: "example.com", + Type: "TXT", + Text: "v=spf1 include:example.com ~all", + TTL: "10m", }, - expected: nil, - expectError: true, + expectError: false, }, - // Empty RecordType { - name: "Empty RecordType", + name: "Invalid TXT record (empty text)", endpoint: &endpoint.Endpoint{ - DNSName: "example.com", - RecordType: "", - Targets: endpoint.NewTargets("192.0.2.1"), - RecordTTL: endpoint.TTL(3600), + DNSName: "invalid.example.com", + RecordType: "TXT", + Targets: endpoint.NewTargets(""), + RecordTTL: endpoint.TTL(600), }, expected: nil, expectError: true, }, - // Empty TTL (should default to "0s") - { - name: "Empty TTL (should default to '0s')", - endpoint: &endpoint.Endpoint{ - DNSName: "example.com", - RecordType: "A", - Targets: endpoint.NewTargets("192.0.2.1"), - // RecordTTL is zero value - }, - expected: &DNSRecord{ - Name: "example.com", - Type: "A", - Address: "192.0.2.1", - TTL: "0s", - }, - expectError: false, - }, - // Invalid TTL value + + // =============================================================== + // PROVIDER-SPECIFIC DATA TEST CASES + // =============================================================== { - name: "Invalid TTL value (negative)", + name: "Invalid provider-specific configuration (unknown field)", endpoint: &endpoint.Endpoint{ DNSName: "example.com", - RecordType: "A", - Targets: endpoint.NewTargets("192.0.2.1"), - RecordTTL: endpoint.TTL(-1), + RecordType: "TXT", + Targets: endpoint.NewTargets("some text"), + ProviderSpecific: endpoint.ProviderSpecific{ + {Name: "unsupported", Value: "value"}, + }, }, expected: nil, expectError: true, }, - // Setting match-subdomain via provider-specific { name: "Setting match-subdomain via provider-specific", endpoint: &endpoint.Endpoint{ @@ -628,7 +744,6 @@ func TestExternalDNSEndpointToDNSRecord(t *testing.T) { }, expectError: false, }, - // Setting address-list via provider-specific { name: "Setting address-list via provider-specific", endpoint: &endpoint.Endpoint{ @@ -649,41 +764,107 @@ func TestExternalDNSEndpointToDNSRecord(t *testing.T) { }, expectError: false, }, - // Multiple provider-specific properties { name: "Multiple provider-specific properties", endpoint: &endpoint.Endpoint{ - DNSName: "multi.example.com", - RecordType: "TXT", - Targets: endpoint.NewTargets("some text"), - RecordTTL: endpoint.TTL(600), + DNSName: "provider.example.com", + RecordType: "A", + Targets: endpoint.NewTargets("192.0.2.3"), + RecordTTL: endpoint.TTL(3600), ProviderSpecific: endpoint.ProviderSpecific{ {Name: "comment", Value: "Test comment"}, - {Name: "address-list", Value: "list1"}, + {Name: "regexp", Value: "^www\\."}, {Name: "match-subdomain", Value: "yes"}, + {Name: "address-list", Value: "list1"}, }, }, expected: &DNSRecord{ - Name: "multi.example.com", - Type: "TXT", - Text: "some text", - TTL: "10m", + Name: "provider.example.com", + Type: "A", + Address: "192.0.2.3", + TTL: "1h", Comment: "Test comment", - AddressList: "list1", + Regexp: "^www\\.", MatchSubdomain: "yes", + AddressList: "list1", }, expectError: false, }, - // Invalid provider-specific name + + // =============================================================== + // DEFAULT VALUES FOR UNSET FIELDS TEST CASES + // =============================================================== { - name: "Invalid provider-specific name", + name: "Empty TTL (should default to '0s')", endpoint: &endpoint.Endpoint{ - DNSName: "invalid.example.com", + DNSName: "example.com", RecordType: "A", Targets: endpoint.NewTargets("192.0.2.1"), - ProviderSpecific: endpoint.ProviderSpecific{ - {Name: "invalid-name", Value: "value"}, - }, + // RecordTTL is zero value + }, + expected: &DNSRecord{ + Name: "example.com", + Type: "A", + Address: "192.0.2.1", + TTL: "0s", + }, + expectError: false, + }, + + // =============================================================== + // GENERIC ERROR CASES + // =============================================================== + { + name: "Record with empty targets", + endpoint: &endpoint.Endpoint{ + DNSName: "empty.example.com", + RecordType: "A", + Targets: endpoint.Targets{}, + RecordTTL: endpoint.TTL(3600), + }, + expected: nil, + expectError: true, + }, + { + name: "Unsupported record type", + endpoint: &endpoint.Endpoint{ + DNSName: "unsupported.example.com", + RecordType: "FWD", + Targets: endpoint.NewTargets("example.com"), + RecordTTL: endpoint.TTL(3600), + }, + expected: nil, + expectError: true, + }, + { + name: "Empty DNSName", + endpoint: &endpoint.Endpoint{ + DNSName: "", + RecordType: "A", + Targets: endpoint.NewTargets("192.0.2.1"), + RecordTTL: endpoint.TTL(3600), + }, + expected: nil, + expectError: true, + }, + { + name: "Empty RecordType", + endpoint: &endpoint.Endpoint{ + DNSName: "example.com", + RecordType: "", + Targets: endpoint.NewTargets("192.0.2.1"), + RecordTTL: endpoint.TTL(3600), + }, + expected: nil, + expectError: true, + }, + { + name: "Invalid TTL value (negative)", + endpoint: &endpoint.Endpoint{ + DNSName: "example.com", + RecordType: "A", + Targets: endpoint.NewTargets("192.0.2.1"), + RecordTTL: endpoint.TTL(-1), }, expected: nil, expectError: true,