Files
realDynDNS/pkg/dnsProvider/ionos/ionos_test.go
Timo Behrendt ac73cf14eb
All checks were successful
CD / Create tag (push) Successful in 14s
CD / test (push) Successful in 32s
CD / Build and push (amd64) (push) Successful in 43s
CD / Build and push (arm64) (push) Successful in 2m23s
CD / Create manifest (push) Successful in 34s
refactor: to use ionosDnsClient (#71)
Reviewed-on: #71
Co-authored-by: Timo Behrendt <t.behrendt@t00n.de>
Co-committed-by: Timo Behrendt <t.behrendt@t00n.de>
2026-01-10 21:00:43 +01:00

1465 lines
38 KiB
Go

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)
}
}
}
}
}