package adguardhome import ( "context" "os" "reflect" "strings" "testing" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/provider" ) type mockAdguardClient struct { rules []string } func (m *mockAdguardClient) GetFilteringRules(_ context.Context) ([]string, error) { return m.rules, nil } func (m *mockAdguardClient) SaveFilteringRules(_ context.Context, rules []string) error { m.rules = rules return nil } func newMockClient() *mockAdguardClient { return &mockAdguardClient{ rules: []string{ "# I am not for external-dns", "1.1.1.1 example.com #$managed by external-dns", "# myresponse notexample.com $managed by external-dns", }, } } func TestAdguardHomeProvider_ApplyChanges(t *testing.T) { p := &AdguardHomeProvider{ client: newMockClient(), ruleFormat: legacyRuleFormat(), } create := []*endpoint.Endpoint{ { DNSName: "example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"2.2.2.2"}, }, } updateNew := []*endpoint.Endpoint{ { DNSName: "example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"3.3.3.3"}, }, } changes := &plan.Changes{ Create: create, UpdateNew: updateNew, UpdateOld: []*endpoint.Endpoint{ { DNSName: "notexample.com", RecordType: endpoint.RecordTypeTXT, Targets: endpoint.Targets{"myresponse"}, }, }, Delete: []*endpoint.Endpoint{ { DNSName: "example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}, }, }, } err := p.ApplyChanges(context.Background(), changes) if err != nil { t.Errorf("failed to apply changes: %v", err) } r, err := p.Records(context.Background()) if err != nil { t.Errorf("failed to fetch records: %v", err) } expected := []*endpoint.Endpoint{ { DNSName: "example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"2.2.2.2", "3.3.3.3"}, }, } if !reflect.DeepEqual(r, expected) { t.Errorf("records do not match: got: %v, expected: %v", r, expected) } // Ensure original rules were kept in place and artificial rule added expectedRules := []string{ "# I am not for external-dns", "2.2.2.2 example.com #$managed by external-dns", "3.3.3.3 example.com #$managed by external-dns", "@@||example.com #$managed by external-dns", } if !reflect.DeepEqual(p.client.(*mockAdguardClient).rules, expectedRules) { t.Errorf("rules do not match: got: %v, expected: %v", p.client.(*mockAdguardClient).rules, expectedRules) } } func TestAdguardHomeProvider_ManagedByRefs(t *testing.T) { // Set up the mock client client := newMockClient() // Set up the first provider with a specific managedBy reference provider1 := &AdguardHomeProvider{ client: client, managedBySuffix: "ref1", ruleFormat: legacyRuleFormat(), } // Set up the second provider with a different managedBy reference provider2 := &AdguardHomeProvider{ client: client, managedBySuffix: "ref2", ruleFormat: legacyRuleFormat(), } // Apply changes with the first provider changes1 := &plan.Changes{ Create: []*endpoint.Endpoint{ { DNSName: "example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"2.2.2.2"}, }, }, } err := provider1.ApplyChanges(context.Background(), changes1) if err != nil { t.Errorf("provider1 failed to apply changes: %v", err) } // Apply changes with the second provider changes2 := &plan.Changes{ Create: []*endpoint.Endpoint{ { DNSName: "example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"3.3.3.3"}, }, }, } err = provider2.ApplyChanges(context.Background(), changes2) if err != nil { t.Errorf("provider2 failed to apply changes: %v", err) } // Fetch records from both providers records1, err := provider1.Records(context.Background()) if err != nil { t.Errorf("provider1 failed to fetch records: %v", err) } records2, err := provider2.Records(context.Background()) if err != nil { t.Errorf("provider2 failed to fetch records: %v", err) } // Validate that the records do not override each other expectedRecords1 := []*endpoint.Endpoint{ { DNSName: "example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"2.2.2.2"}, }, } expectedRecords2 := []*endpoint.Endpoint{ { DNSName: "example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"3.3.3.3"}, }, } if !reflect.DeepEqual(records1, expectedRecords1) { t.Errorf("provider1 records do not match: got: %v, expected: %v", records1, expectedRecords1) } if !reflect.DeepEqual(records2, expectedRecords2) { t.Errorf("provider2 records do not match: got: %v, expected: %v", records2, expectedRecords2) } } func TestAdguardHomeProvider_Records(t *testing.T) { type fields struct { BaseProvider provider.BaseProvider client Client domainFilter *endpoint.DomainFilter DryRun bool } type args struct { ctx context.Context } tests := []struct { name string fields fields args args want []*endpoint.Endpoint wantErr bool }{ { name: "test fetches records", fields: fields{ client: newMockClient(), }, args: args{ ctx: context.Background(), }, want: []*endpoint.Endpoint{ { DNSName: "example.com", RecordType: "A", Targets: endpoint.Targets{"1.1.1.1"}, }, { DNSName: "notexample.com", RecordType: "TXT", Targets: endpoint.Targets{"myresponse"}, }, }, }, { name: "applies domain filter", fields: fields{ client: newMockClient(), domainFilter: endpoint.NewDomainFilter( []string{"example.com"}, ), }, args: args{ ctx: context.Background(), }, want: []*endpoint.Endpoint{ { DNSName: "example.com", RecordType: "A", Targets: endpoint.Targets{"1.1.1.1"}, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := &AdguardHomeProvider{ client: tt.fields.client, domainFilter: tt.fields.domainFilter, } got, err := p.Records(tt.args.ctx) if (err != nil) != tt.wantErr { t.Errorf("AdguardHomeProvider.Records() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("AdguardHomeProvider.Records() = %v, want %v", got, tt.want) } }) } } func TestNewAdguardHomeProvider(t *testing.T) { got, err := NewAdguardHomeProvider(context.Background(), true) if err == nil { t.Errorf("NewAdguardHomeProvider() error = %v", err) return } if got != nil { t.Errorf("NewAdguardHomeProvider() = %v, want %v", got, "not nil") } _ = os.Setenv(envURL, "http://localhost:3000") _ = os.Setenv(envUser, "pw") _ = os.Setenv(envPassword, "user") _, err = NewAdguardHomeProvider(context.Background(), true) if err != nil { t.Errorf("NewAdguardHomeProvider() error = %v", err) } } func TestAdguardHomeProvider_MergeRecords(t *testing.T) { c := newMockClient() p := &AdguardHomeProvider{ client: c, } c.rules = []string{ "1.1.1.1 example.com #$managed by external-dns", "1.2.1.1 example.com #$managed by external-dns", "1.3.1.1 example.com #$managed by external-dns", "1.4.1.1 example.com #$managed by external-dns", } got, err := p.Records(context.Background()) if err != nil { t.Errorf("failed to fetch records: %v", err) } expected := []*endpoint.Endpoint{ { DNSName: "example.com", RecordType: "A", Targets: endpoint.Targets{ "1.1.1.1", "1.2.1.1", "1.3.1.1", "1.4.1.1", }, }, } if !reflect.DeepEqual(got, expected) { t.Errorf("records do not match: got: %v, expected: %v", got, expected) } } func TestAdguardHomeProvider_LabelsHandling(t *testing.T) { c := newMockClient() p := &AdguardHomeProvider{ client: c, ruleFormat: legacyRuleFormat(), } // Test creating endpoints with labels labelsMap := endpoint.Labels{ "owner": "test-owner", "environment": "production", } changes := &plan.Changes{ Create: []*endpoint.Endpoint{ { DNSName: "labeled.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"5.5.5.5"}, Labels: labelsMap, }, { DNSName: "txtlabeled.example.com", RecordType: endpoint.RecordTypeTXT, Targets: endpoint.Targets{"v=spf1"}, Labels: labelsMap, }, }, } err := p.ApplyChanges(context.Background(), changes) if err != nil { t.Errorf("failed to apply changes: %v", err) } // Verify labels are saved in the rules rules := c.rules foundARecord := false foundTXTRecord := false for _, rule := range rules { if strings.Contains(rule, "5.5.5.5 labeled.example.com") { foundARecord = true if !strings.Contains(rule, `"owner":"test-owner"`) { t.Errorf("A record rule missing labels: %s", rule) } } if strings.Contains(rule, "v=spf1 txtlabeled.example.com") { foundTXTRecord = true if !strings.Contains(rule, `"environment":"production"`) { t.Errorf("TXT record rule missing labels: %s", rule) } } } if !foundARecord { t.Error("A record with labels not found in rules") } if !foundTXTRecord { t.Error("TXT record with labels not found in rules") } // Test parsing rules with labels records, err := p.Records(context.Background()) if err != nil { t.Errorf("failed to fetch records: %v", err) } // Find the labeled record var labeledRecord *endpoint.Endpoint for _, rec := range records { if rec.DNSName == "labeled.example.com" { labeledRecord = rec break } } if labeledRecord == nil { t.Fatal("labeled record not found") } if !reflect.DeepEqual(labeledRecord.Labels, labelsMap) { t.Errorf("labels not preserved: got %v, expected %v", labeledRecord.Labels, labelsMap) } // Test backward compatibility with labelless rules c.rules = []string{ "1.1.1.1 old.example.com #$managed by external-dns", "# oldtxt old-txt.example.com $managed by external-dns", } records, err = p.Records(context.Background()) if err != nil { t.Errorf("failed to fetch records from old format: %v", err) } if len(records) != 2 { t.Errorf("expected 2 records, got %d", len(records)) } // Test updating endpoints preserves labels c.rules = []string{ `5.5.5.5 labeled.example.com #$managed by external-dns;labels={"owner":"test-owner","environment":"production"}`, } updateChanges := &plan.Changes{ UpdateOld: []*endpoint.Endpoint{ { DNSName: "labeled.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"5.5.5.5"}, }, }, UpdateNew: []*endpoint.Endpoint{ { DNSName: "labeled.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"6.6.6.6"}, Labels: labelsMap, }, }, } err = p.ApplyChanges(context.Background(), updateChanges) if err != nil { t.Errorf("failed to apply update changes: %v", err) } records, err = p.Records(context.Background()) if err != nil { t.Errorf("failed to fetch records after update: %v", err) } var updatedRecord *endpoint.Endpoint for _, rec := range records { if rec.DNSName == "labeled.example.com" { updatedRecord = rec break } } if updatedRecord == nil { t.Fatal("updated record not found") } if !reflect.DeepEqual(updatedRecord.Labels, labelsMap) { t.Errorf("labels not preserved after update: got %v, expected %v", updatedRecord.Labels, labelsMap) } }