package ionosDnsProvider_test import ( "encoding/json" "net" "net/http" "net/http/httptest" ionosDnsProvider "realdnydns/pkg/dnsProvider/ionos" "testing" ) func TestNew(t *testing.T) { t.Run("API key is required", testNewAPIKeyIsRequired()) t.Run("Base URL is required", testNewBaseURLIsRequired()) t.Run("Valid config", testNewValidConfig()) t.Run("Valid config with custom defaults", testNewValidConfigWithCustomDefaults()) } func testNewAPIKeyIsRequired() func(*testing.T) { return func(t *testing.T) { config := ionosDnsProvider.IONOSConfig{ BaseURL: "https://api.ionos.com", } _, err := ionosDnsProvider.NewIonos(&config) if err == nil { t.Error("Expected error, got nil") } } } func testNewBaseURLIsRequired() func(*testing.T) { return func(t *testing.T) { config := ionosDnsProvider.IONOSConfig{ APIKey: "1234", } _, err := ionosDnsProvider.NewIonos(&config) if err == nil { t.Error("Expected error, got nil") } } } func testNewValidConfig() func(*testing.T) { return func(t *testing.T) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) })) defer mockServer.Close() config := ionosDnsProvider.IONOSConfig{ APIKey: "1234", BaseURL: mockServer.URL, } _, err := ionosDnsProvider.NewIonos(&config) if err != nil { t.Errorf("Expected nil, got %v", err) } } } func testNewValidConfigWithCustomDefaults() func(*testing.T) { return func(t *testing.T) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) })) defer mockServer.Close() customTTL := 600 customPrio := 10 config := ionosDnsProvider.IONOSConfig{ APIKey: "1234", BaseURL: mockServer.URL, DefaultTTL: &customTTL, DefaultPrio: &customPrio, } _, err := ionosDnsProvider.NewIonos(&config) if err != nil { t.Errorf("Expected nil, got %v", err) } } } // Helper function to create a mock server that responds to IONOS API calls func createMockServer() *httptest.Server { zoneId := "1234567890" recordIdSub := "abcdefghij" recordIdTLD := "jihgfedcba" zonesResponse := []map[string]any{ { "name": "example.com", "id": zoneId, "type": "NATIVE", }, { "name": "notTheExample.org", "id": "0987654321", "type": "SLAVE", }, } zoneResponse := map[string]any{ "id": zoneId, "name": "example.com", "type": "NATIVE", "records": []map[string]any{ { "id": recordIdTLD, "name": "example.com", "rootName": "example.com", "type": "A", "content": "127.0.0.1", "ttl": 300, "prio": 0, "disabled": false, }, { "id": recordIdSub, "name": "sub.example.com", "rootName": "example.com", "type": "A", "content": "127.0.0.2", "ttl": 300, "prio": 0, "disabled": false, }, }, } recordResponseSub := map[string]any{ "id": recordIdSub, "name": "sub.example.com", "rootName": "example.com", "type": "A", "content": "127.0.0.2", "ttl": 300, "prio": 0, "disabled": false, } recordResponseTLD := map[string]any{ "id": recordIdTLD, "name": "example.com", "rootName": "example.com", "type": "A", "content": "127.0.0.1", "ttl": 300, "prio": 0, "disabled": false, } return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if r.Method == "GET" { switch r.URL.Path { case "/v1/zones": json.NewEncoder(w).Encode(zonesResponse) case "/v1/zones/" + zoneId: // Check query parameters recordName := r.URL.Query().Get("recordName") recordType := r.URL.Query().Get("recordType") if recordName != "" && recordType == "A" { json.NewEncoder(w).Encode(zoneResponse) } else { w.WriteHeader(400) } case "/v1/zones/" + zoneId + "/records/" + recordIdSub: json.NewEncoder(w).Encode(recordResponseSub) case "/v1/zones/" + zoneId + "/records/" + recordIdTLD: json.NewEncoder(w).Encode(recordResponseTLD) default: w.WriteHeader(404) } } else if r.Method == "PUT" { switch r.URL.Path { case "/v1/zones/" + zoneId + "/records/" + recordIdSub: // Return updated record json.NewEncoder(w).Encode(recordResponseSub) case "/v1/zones/" + zoneId + "/records/" + recordIdTLD: // Return updated record json.NewEncoder(w).Encode(recordResponseTLD) default: w.WriteHeader(404) } } else { w.WriteHeader(404) } })) } func TestGetRecord(t *testing.T) { t.Run("Success for subdomain", testGetRecordSuccessSubdomain()) t.Run("Success for TLD", testGetRecordSuccessTLD()) t.Run("Success for @ subdomain", testGetRecordSuccessAtSymbol()) t.Run("Error when zone not found", testGetRecordZoneNotFound()) t.Run("Error when record not found", testGetRecordRecordNotFound()) t.Run("Error when API call fails", testGetRecordAPICallFails()) t.Run("Error when record name mismatch", testGetRecordNameMismatch()) t.Run("Error when GetZonesWithResponse returns non-200", testGetRecordGetZonesWithResponseNon200()) t.Run("Error when zone id is nil", testGetRecordZoneIdNil()) t.Run("Error when zone id is empty", testGetRecordZoneIdEmpty()) t.Run("Error when GetRecordWithResponse returns non-200", testGetRecordGetRecordWithResponseNon200()) t.Run("Error when record content is nil", testGetRecordContentNil()) t.Run("Error when record content is empty", testGetRecordContentEmpty()) t.Run("Success when record ttl is nil (uses default)", testGetRecordTtlNil()) t.Run("Success when record prio is nil (uses default)", testGetRecordPrioNil()) t.Run("Success when record disabled is nil (uses default)", testGetRecordDisabledNil()) } func testGetRecordSuccessSubdomain() func(*testing.T) { return func(t *testing.T) { mockServer := createMockServer() defer mockServer.Close() config := ionosDnsProvider.IONOSConfig{ APIKey: "test-key", BaseURL: mockServer.URL, } provider, err := ionosDnsProvider.NewIonos(&config) if err != nil { t.Fatalf("Failed to create provider: %v", err) } record, err := provider.GetRecord("example.com", "sub") if err != nil { t.Errorf("Expected nil, got %v", err) } if record.Domain != "sub.example.com" { t.Errorf("Expected sub.example.com, got %s", record.Domain) } if record.IP != "127.0.0.2" { t.Errorf("Expected 127.0.0.2, got %s", record.IP) } if record.TTL != 300 { t.Errorf("Expected 300, got %d", record.TTL) } if record.Prio != 0 { t.Errorf("Expected 0, got %d", record.Prio) } if record.Disabled { t.Error("Expected false, got true") } } } func testGetRecordSuccessTLD() func(*testing.T) { return func(t *testing.T) { mockServer := createMockServer() defer mockServer.Close() config := ionosDnsProvider.IONOSConfig{ APIKey: "test-key", BaseURL: mockServer.URL, } provider, err := ionosDnsProvider.NewIonos(&config) if err != nil { t.Fatalf("Failed to create provider: %v", err) } record, err := provider.GetRecord("example.com", "") if err != nil { t.Errorf("Expected nil, got %v", err) } if record.Domain != "example.com" { t.Errorf("Expected example.com, got %s", record.Domain) } if record.IP != "127.0.0.1" { t.Errorf("Expected 127.0.0.1, got %s", record.IP) } } } func testGetRecordSuccessAtSymbol() func(*testing.T) { return func(t *testing.T) { mockServer := createMockServer() defer mockServer.Close() config := ionosDnsProvider.IONOSConfig{ APIKey: "test-key", BaseURL: mockServer.URL, } provider, err := ionosDnsProvider.NewIonos(&config) if err != nil { t.Fatalf("Failed to create provider: %v", err) } record, err := provider.GetRecord("example.com", "@") if err != nil { t.Errorf("Expected nil, got %v", err) } if record.Domain != "example.com" { t.Errorf("Expected example.com, got %s", record.Domain) } if record.IP != "127.0.0.1" { t.Errorf("Expected 127.0.0.1, got %s", record.IP) } } } func testGetRecordZoneNotFound() func(*testing.T) { return func(t *testing.T) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if r.URL.Path == "/v1/zones" { json.NewEncoder(w).Encode([]map[string]any{}) } else { w.WriteHeader(404) } })) defer mockServer.Close() config := ionosDnsProvider.IONOSConfig{ APIKey: "test-key", BaseURL: mockServer.URL, } provider, err := ionosDnsProvider.NewIonos(&config) if err != nil { t.Fatalf("Failed to create provider: %v", err) } _, err = provider.GetRecord("nonexistent.com", "sub") if err == nil { t.Error("Expected error, got nil") } } } func testGetRecordRecordNotFound() func(*testing.T) { return func(t *testing.T) { zoneId := "1234567890" mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch r.URL.Path { case "/v1/zones": json.NewEncoder(w).Encode([]map[string]any{ { "name": "example.com", "id": zoneId, "type": "NATIVE", }, }) case "/v1/zones/" + zoneId: zoneResponse := map[string]any{ "id": zoneId, "name": "example.com", "type": "NATIVE", "records": []map[string]any{}, } json.NewEncoder(w).Encode(zoneResponse) default: w.WriteHeader(404) } })) defer mockServer.Close() config := ionosDnsProvider.IONOSConfig{ APIKey: "test-key", BaseURL: mockServer.URL, } provider, err := ionosDnsProvider.NewIonos(&config) if err != nil { t.Fatalf("Failed to create provider: %v", err) } _, err = provider.GetRecord("example.com", "nonexistent") if err == nil { t.Error("Expected error, got nil") } } } func testGetRecordAPICallFails() func(*testing.T) { return func(t *testing.T) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) })) defer mockServer.Close() config := ionosDnsProvider.IONOSConfig{ APIKey: "test-key", BaseURL: mockServer.URL, } provider, err := ionosDnsProvider.NewIonos(&config) if err != nil { t.Fatalf("Failed to create provider: %v", err) } _, err = provider.GetRecord("example.com", "sub") if err == nil { t.Error("Expected error, got nil") } } } func testGetRecordNameMismatch() func(*testing.T) { return func(t *testing.T) { zoneId := "1234567890" recordId := "abcdefghij" mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch r.URL.Path { case "/v1/zones": json.NewEncoder(w).Encode([]map[string]any{ { "name": "example.com", "id": zoneId, "type": "NATIVE", }, }) case "/v1/zones/" + zoneId: zoneResponse := map[string]any{ "id": zoneId, "name": "example.com", "type": "NATIVE", "records": []map[string]any{ { "id": recordId, "name": "sub.example.com", "type": "A", }, }, } json.NewEncoder(w).Encode(zoneResponse) case "/v1/zones/" + zoneId + "/records/" + recordId: // Return record with wrong name recordResponse := map[string]any{ "id": recordId, "name": "wrong.example.com", "rootName": "example.com", "type": "A", "content": "127.0.0.1", "ttl": 300, "prio": 0, "disabled": false, } json.NewEncoder(w).Encode(recordResponse) default: w.WriteHeader(404) } })) defer mockServer.Close() config := ionosDnsProvider.IONOSConfig{ APIKey: "test-key", BaseURL: mockServer.URL, } provider, err := ionosDnsProvider.NewIonos(&config) if err != nil { t.Fatalf("Failed to create provider: %v", err) } _, err = provider.GetRecord("example.com", "sub") if err == nil { t.Error("Expected error, got nil") } } } func testGetRecordGetZonesWithResponseNon200() func(*testing.T) { return func(t *testing.T) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/v1/zones" { w.WriteHeader(401) // Unauthorized } else { w.WriteHeader(404) } })) defer mockServer.Close() config := ionosDnsProvider.IONOSConfig{ APIKey: "test-key", BaseURL: mockServer.URL, } provider, err := ionosDnsProvider.NewIonos(&config) if err != nil { t.Fatalf("Failed to create provider: %v", err) } _, err = provider.GetRecord("example.com", "sub") if err == nil { t.Error("Expected error, got nil") } if err.Error() != "failed to get zones" { t.Errorf("Expected 'failed to get zones', got %v", err) } } } func testGetRecordZoneIdNil() func(*testing.T) { return func(t *testing.T) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if r.URL.Path == "/v1/zones" { // Return zone with nil id zonesResponse := []map[string]any{ { "name": "example.com", "id": nil, "type": "NATIVE", }, } json.NewEncoder(w).Encode(zonesResponse) } else { w.WriteHeader(404) } })) defer mockServer.Close() config := ionosDnsProvider.IONOSConfig{ APIKey: "test-key", BaseURL: mockServer.URL, } provider, err := ionosDnsProvider.NewIonos(&config) if err != nil { t.Fatalf("Failed to create provider: %v", err) } _, err = provider.GetRecord("example.com", "sub") if err == nil { t.Error("Expected error, got nil") } if err.Error() != "zone id is empty" { t.Errorf("Expected 'zone id is empty', got %v", err) } } } func testGetRecordZoneIdEmpty() func(*testing.T) { return func(t *testing.T) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if r.URL.Path == "/v1/zones" { // Return zone with empty id zonesResponse := []map[string]any{ { "name": "example.com", "id": "", "type": "NATIVE", }, } json.NewEncoder(w).Encode(zonesResponse) } else { w.WriteHeader(404) } })) defer mockServer.Close() config := ionosDnsProvider.IONOSConfig{ APIKey: "test-key", BaseURL: mockServer.URL, } provider, err := ionosDnsProvider.NewIonos(&config) if err != nil { t.Fatalf("Failed to create provider: %v", err) } _, err = provider.GetRecord("example.com", "sub") if err == nil { t.Error("Expected error, got nil") } if err.Error() != "zone id is empty" { t.Errorf("Expected 'zone id is empty', got %v", err) } } } func testGetRecordGetRecordWithResponseNon200() func(*testing.T) { return func(t *testing.T) { zoneId := "1234567890" recordId := "abcdefghij" mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch r.URL.Path { case "/v1/zones": json.NewEncoder(w).Encode([]map[string]any{ { "name": "example.com", "id": zoneId, "type": "NATIVE", }, }) case "/v1/zones/" + zoneId: zoneResponse := map[string]any{ "id": zoneId, "name": "example.com", "type": "NATIVE", "records": []map[string]any{ { "id": recordId, "name": "sub.example.com", "type": "A", }, }, } json.NewEncoder(w).Encode(zoneResponse) case "/v1/zones/" + zoneId + "/records/" + recordId: // Return non-200 status w.WriteHeader(500) default: w.WriteHeader(404) } })) defer mockServer.Close() config := ionosDnsProvider.IONOSConfig{ APIKey: "test-key", BaseURL: mockServer.URL, } provider, err := ionosDnsProvider.NewIonos(&config) if err != nil { t.Fatalf("Failed to create provider: %v", err) } _, err = provider.GetRecord("example.com", "sub") if err == nil { t.Error("Expected error, got nil") } if err.Error() != "failed to get record" { t.Errorf("Expected 'failed to get record', got %v", err) } } } func testGetRecordContentNil() func(*testing.T) { return func(t *testing.T) { zoneId := "1234567890" recordId := "abcdefghij" mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case r.Method == "GET" && r.URL.Path == "/v1/zones": json.NewEncoder(w).Encode([]map[string]any{ { "name": "example.com", "id": zoneId, "type": "NATIVE", }, }) case r.Method == "GET" && r.URL.Path == "/v1/zones/"+zoneId: zoneResponse := map[string]any{ "id": zoneId, "name": "example.com", "type": "NATIVE", "records": []map[string]any{ { "id": recordId, "name": "sub.example.com", "type": "A", }, }, } json.NewEncoder(w).Encode(zoneResponse) case r.Method == "GET" && r.URL.Path == "/v1/zones/"+zoneId+"/records/"+recordId: recordResponse := map[string]any{ "id": recordId, "name": "sub.example.com", "rootName": "example.com", "type": "A", "content": nil, "ttl": 300, "prio": 0, "disabled": false, } json.NewEncoder(w).Encode(recordResponse) default: w.WriteHeader(404) } })) defer mockServer.Close() config := ionosDnsProvider.IONOSConfig{ APIKey: "test-key", BaseURL: mockServer.URL, } provider, err := ionosDnsProvider.NewIonos(&config) if err != nil { t.Fatalf("Failed to create provider: %v", err) } _, err = provider.GetRecord("example.com", "sub") if err == nil { t.Error("Expected error, got nil") } if err.Error() != "record content is empty" { t.Errorf("Expected 'record content is empty', got %v", err) } } } func testGetRecordContentEmpty() func(*testing.T) { return func(t *testing.T) { zoneId := "1234567890" recordId := "abcdefghij" mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case r.Method == "GET" && r.URL.Path == "/v1/zones": json.NewEncoder(w).Encode([]map[string]any{ { "name": "example.com", "id": zoneId, "type": "NATIVE", }, }) case r.Method == "GET" && r.URL.Path == "/v1/zones/"+zoneId: zoneResponse := map[string]any{ "id": zoneId, "name": "example.com", "type": "NATIVE", "records": []map[string]any{ { "id": recordId, "name": "sub.example.com", "type": "A", }, }, } json.NewEncoder(w).Encode(zoneResponse) case r.Method == "GET" && r.URL.Path == "/v1/zones/"+zoneId+"/records/"+recordId: recordResponse := map[string]any{ "id": recordId, "name": "sub.example.com", "rootName": "example.com", "type": "A", "content": "", "ttl": 300, "prio": 0, "disabled": false, } json.NewEncoder(w).Encode(recordResponse) default: w.WriteHeader(404) } })) defer mockServer.Close() config := ionosDnsProvider.IONOSConfig{ APIKey: "test-key", BaseURL: mockServer.URL, } provider, err := ionosDnsProvider.NewIonos(&config) if err != nil { t.Fatalf("Failed to create provider: %v", err) } _, err = provider.GetRecord("example.com", "sub") if err == nil { t.Error("Expected error, got nil") } if err.Error() != "record content is empty" { t.Errorf("Expected 'record content is empty', got %v", err) } } } func testGetRecordTtlNil() func(*testing.T) { return func(t *testing.T) { zoneId := "1234567890" recordId := "abcdefghij" mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case r.Method == "GET" && r.URL.Path == "/v1/zones": json.NewEncoder(w).Encode([]map[string]any{ { "name": "example.com", "id": zoneId, "type": "NATIVE", }, }) case r.Method == "GET" && r.URL.Path == "/v1/zones/"+zoneId: zoneResponse := map[string]any{ "id": zoneId, "name": "example.com", "type": "NATIVE", "records": []map[string]any{ { "id": recordId, "name": "sub.example.com", "type": "A", }, }, } json.NewEncoder(w).Encode(zoneResponse) case r.Method == "GET" && r.URL.Path == "/v1/zones/"+zoneId+"/records/"+recordId: recordResponse := map[string]any{ "id": recordId, "name": "sub.example.com", "rootName": "example.com", "type": "A", "content": "127.0.0.1", "ttl": nil, "prio": 0, "disabled": false, } json.NewEncoder(w).Encode(recordResponse) case r.Method == "PUT" && r.URL.Path == "/v1/zones/"+zoneId+"/records/"+recordId: // Return updated record with TTL set recordResponse := map[string]any{ "id": recordId, "name": "sub.example.com", "rootName": "example.com", "type": "A", "content": "127.0.0.1", "ttl": 300, "prio": 0, "disabled": false, } json.NewEncoder(w).Encode(recordResponse) default: w.WriteHeader(404) } })) defer mockServer.Close() config := ionosDnsProvider.IONOSConfig{ APIKey: "test-key", BaseURL: mockServer.URL, } provider, err := ionosDnsProvider.NewIonos(&config) if err != nil { t.Fatalf("Failed to create provider: %v", err) } record, err := provider.GetRecord("example.com", "sub") if err != nil { t.Errorf("Expected nil, got %v", err) } if record.TTL != 300 { t.Errorf("Expected TTL to be 300 (default after patch), got %d", record.TTL) } if record.IP != "127.0.0.1" { t.Errorf("Expected IP to be 127.0.0.1, got %s", record.IP) } } } func testGetRecordPrioNil() func(*testing.T) { return func(t *testing.T) { zoneId := "1234567890" recordId := "abcdefghij" mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case r.Method == "GET" && r.URL.Path == "/v1/zones": json.NewEncoder(w).Encode([]map[string]any{ { "name": "example.com", "id": zoneId, "type": "NATIVE", }, }) case r.Method == "GET" && r.URL.Path == "/v1/zones/"+zoneId: zoneResponse := map[string]any{ "id": zoneId, "name": "example.com", "type": "NATIVE", "records": []map[string]any{ { "id": recordId, "name": "sub.example.com", "type": "A", }, }, } json.NewEncoder(w).Encode(zoneResponse) case r.Method == "GET" && r.URL.Path == "/v1/zones/"+zoneId+"/records/"+recordId: recordResponse := map[string]any{ "id": recordId, "name": "sub.example.com", "rootName": "example.com", "type": "A", "content": "127.0.0.1", "ttl": 300, "prio": nil, "disabled": false, } json.NewEncoder(w).Encode(recordResponse) case r.Method == "PUT" && r.URL.Path == "/v1/zones/"+zoneId+"/records/"+recordId: // Return updated record with Prio set recordResponse := map[string]any{ "id": recordId, "name": "sub.example.com", "rootName": "example.com", "type": "A", "content": "127.0.0.1", "ttl": 300, "prio": 0, "disabled": false, } json.NewEncoder(w).Encode(recordResponse) default: w.WriteHeader(404) } })) defer mockServer.Close() config := ionosDnsProvider.IONOSConfig{ APIKey: "test-key", BaseURL: mockServer.URL, } provider, err := ionosDnsProvider.NewIonos(&config) if err != nil { t.Fatalf("Failed to create provider: %v", err) } record, err := provider.GetRecord("example.com", "sub") if err != nil { t.Errorf("Expected nil, got %v", err) } if record.Prio != 0 { t.Errorf("Expected Prio to be 0 (default after patch), got %d", record.Prio) } if record.IP != "127.0.0.1" { t.Errorf("Expected IP to be 127.0.0.1, got %s", record.IP) } } } func testGetRecordDisabledNil() func(*testing.T) { return func(t *testing.T) { zoneId := "1234567890" recordId := "abcdefghij" mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case r.Method == "GET" && r.URL.Path == "/v1/zones": json.NewEncoder(w).Encode([]map[string]any{ { "name": "example.com", "id": zoneId, "type": "NATIVE", }, }) case r.Method == "GET" && r.URL.Path == "/v1/zones/"+zoneId: zoneResponse := map[string]any{ "id": zoneId, "name": "example.com", "type": "NATIVE", "records": []map[string]any{ { "id": recordId, "name": "sub.example.com", "type": "A", }, }, } json.NewEncoder(w).Encode(zoneResponse) case r.Method == "GET" && r.URL.Path == "/v1/zones/"+zoneId+"/records/"+recordId: recordResponse := map[string]any{ "id": recordId, "name": "sub.example.com", "rootName": "example.com", "type": "A", "content": "127.0.0.1", "ttl": 300, "prio": 0, "disabled": nil, } json.NewEncoder(w).Encode(recordResponse) case r.Method == "PUT" && r.URL.Path == "/v1/zones/"+zoneId+"/records/"+recordId: // Return updated record with Disabled set recordResponse := map[string]any{ "id": recordId, "name": "sub.example.com", "rootName": "example.com", "type": "A", "content": "127.0.0.1", "ttl": 300, "prio": 0, "disabled": false, } json.NewEncoder(w).Encode(recordResponse) default: w.WriteHeader(404) } })) defer mockServer.Close() config := ionosDnsProvider.IONOSConfig{ APIKey: "test-key", BaseURL: mockServer.URL, } provider, err := ionosDnsProvider.NewIonos(&config) if err != nil { t.Fatalf("Failed to create provider: %v", err) } record, err := provider.GetRecord("example.com", "sub") if err != nil { t.Errorf("Expected nil, got %v", err) } if record.Disabled != false { t.Errorf("Expected Disabled to be false (default after patch), got %v", record.Disabled) } if record.IP != "127.0.0.1" { t.Errorf("Expected IP to be 127.0.0.1, got %s", record.IP) } } } func TestUpdateRecord(t *testing.T) { t.Run("Success for subdomain", testUpdateRecordSuccessSubdomain()) t.Run("Success for TLD", testUpdateRecordSuccessTLD()) t.Run("Error when zone not found", testUpdateRecordZoneNotFound()) t.Run("Error when record not found", testUpdateRecordRecordNotFound()) t.Run("Error when UpdateRecordWithResponse returns non-200", testUpdateRecordAPICallFails()) t.Run("Error when record response is empty", testUpdateRecordEmptyResponse()) } func testUpdateRecordSuccessSubdomain() func(*testing.T) { return func(t *testing.T) { zoneId := "1234567890" recordId := "abcdefghij" mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case r.Method == "GET" && r.URL.Path == "/v1/zones": json.NewEncoder(w).Encode([]map[string]any{ { "name": "example.com", "id": zoneId, "type": "NATIVE", }, }) case r.Method == "GET" && r.URL.Path == "/v1/zones/"+zoneId: recordName := r.URL.Query().Get("recordName") if recordName == "sub.example.com" { zoneResponse := map[string]any{ "id": zoneId, "name": "example.com", "type": "NATIVE", "records": []map[string]any{ { "id": recordId, "name": "sub.example.com", "type": "A", }, }, } json.NewEncoder(w).Encode(zoneResponse) } else { w.WriteHeader(400) } case r.Method == "PUT" && r.URL.Path == "/v1/zones/"+zoneId+"/records/"+recordId: recordResponse := map[string]any{ "id": recordId, "name": "sub.example.com", "rootName": "example.com", "type": "A", "content": "192.168.1.1", "ttl": 60, "prio": 0, "disabled": false, } json.NewEncoder(w).Encode(recordResponse) default: w.WriteHeader(404) } })) defer mockServer.Close() config := ionosDnsProvider.IONOSConfig{ APIKey: "test-key", BaseURL: mockServer.URL, } provider, err := ionosDnsProvider.NewIonos(&config) if err != nil { t.Fatalf("Failed to create provider: %v", err) } record, err := provider.UpdateRecord("example.com", "sub", net.ParseIP("192.168.1.1"), 60, 0, false) if err != nil { t.Errorf("Expected nil, got %v", err) } if record.Domain != "sub.example.com" { t.Errorf("Expected sub.example.com, got %s", record.Domain) } if record.IP != "192.168.1.1" { t.Errorf("Expected 192.168.1.1, got %s", record.IP) } if record.TTL != 60 { t.Errorf("Expected 60, got %d", record.TTL) } if record.Prio != 0 { t.Errorf("Expected 0, got %d", record.Prio) } if record.Disabled { t.Error("Expected false, got true") } } } func testUpdateRecordSuccessTLD() func(*testing.T) { return func(t *testing.T) { zoneId := "1234567890" recordId := "jihgfedcba" mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case r.Method == "GET" && r.URL.Path == "/v1/zones": json.NewEncoder(w).Encode([]map[string]any{ { "name": "example.com", "id": zoneId, "type": "NATIVE", }, }) case r.Method == "GET" && r.URL.Path == "/v1/zones/"+zoneId: recordName := r.URL.Query().Get("recordName") if recordName == "example.com" { zoneResponse := map[string]any{ "id": zoneId, "name": "example.com", "type": "NATIVE", "records": []map[string]any{ { "id": recordId, "name": "example.com", "type": "A", }, }, } json.NewEncoder(w).Encode(zoneResponse) } else { w.WriteHeader(400) } case r.Method == "PUT" && r.URL.Path == "/v1/zones/"+zoneId+"/records/"+recordId: recordResponse := map[string]any{ "id": recordId, "name": "example.com", "rootName": "example.com", "type": "A", "content": "192.168.1.1", "ttl": 300, "prio": 0, "disabled": false, } json.NewEncoder(w).Encode(recordResponse) default: w.WriteHeader(404) } })) defer mockServer.Close() config := ionosDnsProvider.IONOSConfig{ APIKey: "test-key", BaseURL: mockServer.URL, } provider, err := ionosDnsProvider.NewIonos(&config) if err != nil { t.Fatalf("Failed to create provider: %v", err) } record, err := provider.UpdateRecord("example.com", "", net.ParseIP("192.168.1.1"), 300, 0, false) if err != nil { t.Errorf("Expected nil, got %v", err) } if record.Domain != "example.com" { t.Errorf("Expected example.com, got %s", record.Domain) } if record.IP != "192.168.1.1" { t.Errorf("Expected 192.168.1.1, got %s", record.IP) } } } func testUpdateRecordZoneNotFound() func(*testing.T) { return func(t *testing.T) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if r.URL.Path == "/v1/zones" { json.NewEncoder(w).Encode([]map[string]any{}) } else { w.WriteHeader(404) } })) defer mockServer.Close() config := ionosDnsProvider.IONOSConfig{ APIKey: "test-key", BaseURL: mockServer.URL, } provider, err := ionosDnsProvider.NewIonos(&config) if err != nil { t.Fatalf("Failed to create provider: %v", err) } _, err = provider.UpdateRecord("nonexistent.com", "sub", net.ParseIP("192.168.1.1"), 60, 0, false) if err == nil { t.Error("Expected error, got nil") } } } func testUpdateRecordRecordNotFound() func(*testing.T) { return func(t *testing.T) { zoneId := "1234567890" mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case r.Method == "GET" && r.URL.Path == "/v1/zones": json.NewEncoder(w).Encode([]map[string]any{ { "name": "example.com", "id": zoneId, "type": "NATIVE", }, }) case r.Method == "GET" && r.URL.Path == "/v1/zones/"+zoneId: zoneResponse := map[string]any{ "id": zoneId, "name": "example.com", "type": "NATIVE", "records": []map[string]any{}, } json.NewEncoder(w).Encode(zoneResponse) default: w.WriteHeader(404) } })) defer mockServer.Close() config := ionosDnsProvider.IONOSConfig{ APIKey: "test-key", BaseURL: mockServer.URL, } provider, err := ionosDnsProvider.NewIonos(&config) if err != nil { t.Fatalf("Failed to create provider: %v", err) } _, err = provider.UpdateRecord("example.com", "nonexistent", net.ParseIP("192.168.1.1"), 60, 0, false) if err == nil { t.Error("Expected error, got nil") } } } func testUpdateRecordAPICallFails() func(*testing.T) { return func(t *testing.T) { zoneId := "1234567890" recordId := "abcdefghij" mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case r.Method == "GET" && r.URL.Path == "/v1/zones": json.NewEncoder(w).Encode([]map[string]any{ { "name": "example.com", "id": zoneId, "type": "NATIVE", }, }) case r.Method == "GET" && r.URL.Path == "/v1/zones/"+zoneId: recordName := r.URL.Query().Get("recordName") if recordName == "sub.example.com" { zoneResponse := map[string]any{ "id": zoneId, "name": "example.com", "type": "NATIVE", "records": []map[string]any{ { "id": recordId, "name": "sub.example.com", "type": "A", }, }, } json.NewEncoder(w).Encode(zoneResponse) } else { w.WriteHeader(400) } case r.Method == "PUT" && r.URL.Path == "/v1/zones/"+zoneId+"/records/"+recordId: w.WriteHeader(500) default: w.WriteHeader(404) } })) defer mockServer.Close() config := ionosDnsProvider.IONOSConfig{ APIKey: "test-key", BaseURL: mockServer.URL, } provider, err := ionosDnsProvider.NewIonos(&config) if err != nil { t.Fatalf("Failed to create provider: %v", err) } _, err = provider.UpdateRecord("example.com", "sub", net.ParseIP("192.168.1.1"), 60, 0, false) if err == nil { t.Error("Expected error, got nil") } if err.Error() != "failed to update record" { t.Errorf("Expected 'failed to update record', got %v", err) } } } func testUpdateRecordEmptyResponse() func(*testing.T) { return func(t *testing.T) { zoneId := "1234567890" recordId := "abcdefghij" mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case r.Method == "GET" && r.URL.Path == "/v1/zones": json.NewEncoder(w).Encode([]map[string]any{ { "name": "example.com", "id": zoneId, "type": "NATIVE", }, }) case r.Method == "GET" && r.URL.Path == "/v1/zones/"+zoneId: recordName := r.URL.Query().Get("recordName") if recordName == "sub.example.com" { zoneResponse := map[string]any{ "id": zoneId, "name": "example.com", "type": "NATIVE", "records": []map[string]any{ { "id": recordId, "name": "sub.example.com", "type": "A", }, }, } json.NewEncoder(w).Encode(zoneResponse) } else { w.WriteHeader(400) } case r.Method == "PUT" && r.URL.Path == "/v1/zones/"+zoneId+"/records/"+recordId: // Return record with nil fields recordResponse := map[string]any{ "id": recordId, "name": nil, "rootName": "example.com", "type": "A", "content": nil, "ttl": nil, "prio": nil, "disabled": nil, } json.NewEncoder(w).Encode(recordResponse) default: w.WriteHeader(404) } })) defer mockServer.Close() config := ionosDnsProvider.IONOSConfig{ APIKey: "test-key", BaseURL: mockServer.URL, } provider, err := ionosDnsProvider.NewIonos(&config) if err != nil { t.Fatalf("Failed to create provider: %v", err) } _, err = provider.UpdateRecord("example.com", "sub", net.ParseIP("192.168.1.1"), 60, 0, false) if err == nil { t.Error("Expected error, got nil") } if err.Error() != "record is empty" { t.Errorf("Expected 'record is empty', got %v", err) } } } func TestGetZoneIdCaching(t *testing.T) { t.Run("Zone ID is cached", testGetZoneIdCaching()) } func testGetZoneIdCaching() func(*testing.T) { return func(t *testing.T) { callCount := 0 zoneId := "1234567890" mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if r.URL.Path == "/v1/zones" { callCount++ json.NewEncoder(w).Encode([]map[string]any{ { "name": "example.com", "id": zoneId, "type": "NATIVE", }, }) } else { w.WriteHeader(404) } })) defer mockServer.Close() config := ionosDnsProvider.IONOSConfig{ APIKey: "test-key", BaseURL: mockServer.URL, } provider, err := ionosDnsProvider.NewIonos(&config) if err != nil { t.Fatalf("Failed to create provider: %v", err) } // First call should fetch from API _, err = provider.GetRecord("example.com", "sub") if err == nil { // Second call should use cached zone ID _, err = provider.GetRecord("example.com", "") if err == nil { // The API should only be called once for zones if callCount > 1 { t.Errorf("Expected zone API to be called once, but was called %d times", callCount) } } } } }