diff --git a/internal/dynuclient/client.go b/internal/dynuclient/client.go index fd18afb..0111fe1 100644 --- a/internal/dynuclient/client.go +++ b/internal/dynuclient/client.go @@ -322,18 +322,18 @@ type dnsRecordUpsertPayload struct { } func buildDNSRecordUpsertPayload(recordType string, nodeName string, content *string, ttl int64, state *bool, group string, host string) dnsRecordUpsertPayload { + normalizedType := strings.ToUpper(strings.TrimSpace(recordType)) normalizedContent := normalizeOptionalContent(content) payload := dnsRecordUpsertPayload{ NodeName: nodeName, - RecordType: recordType, - Content: normalizedContent, + RecordType: normalizedType, TTL: ttl, State: state, Group: group, Host: host, } - switch strings.ToUpper(strings.TrimSpace(recordType)) { + switch normalizedType { case "A": if normalizedContent != nil { payload.IPv4Address = *normalizedContent @@ -346,6 +346,8 @@ func buildDNSRecordUpsertPayload(recordType string, nodeName string, content *st if payload.Host == "" && normalizedContent != nil { payload.Host = *normalizedContent } + default: + payload.Content = normalizedContent } return payload diff --git a/internal/dynuclient/client_test.go b/internal/dynuclient/client_test.go index 36f0f05..e2f22e0 100644 --- a/internal/dynuclient/client_test.go +++ b/internal/dynuclient/client_test.go @@ -189,6 +189,151 @@ func TestClientCreateDNSRecordSendsIPv4AddressForARecord(t *testing.T) { if captured["ipv4Address"] != "167.179.167.166" { t.Fatalf("expected ipv4Address in payload, got %#v", captured) } + if _, ok := captured["content"]; ok { + t.Fatalf("expected content to be omitted for A record, got %#v", captured["content"]) + } + if _, ok := captured["ipv6Address"]; ok { + t.Fatalf("expected ipv6Address to be omitted for A record, got %#v", captured["ipv6Address"]) + } +} + +func TestClientUpdateDNSRecordSendsIPv6AddressForAAAARecord(t *testing.T) { + var captured map[string]any + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut || r.URL.Path != "/dns/1001/record/2002" { + t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path) + } + if err := json.NewDecoder(r.Body).Decode(&captured); err != nil { + t.Fatalf("failed to decode payload: %v", err) + } + _ = json.NewEncoder(w).Encode(map[string]any{ + "statusCode": 200, + "id": 2002, + "domainId": 1001, + "domainName": "example.com", + "nodeName": "www", + "hostname": "www.example.com", + "recordType": "AAAA", + "content": "2001:db8::123", + "ttl": 90, + "state": true, + }) + })) + defer server.Close() + + client := dynuclient.New("test-key", dynuclient.WithBaseURL(server.URL), dynuclient.WithHTTPClient(server.Client())) + _, err := client.UpdateDNSRecord(context.Background(), 1001, 2002, dynuclient.UpdateDNSRecordRequest{ + NodeName: "www", + RecordType: "AAAA", + Content: stringPointer("2001:db8::123"), + TTL: 90, + }) + if err != nil { + t.Fatalf("UpdateDNSRecord() error = %v", err) + } + + if captured["recordType"] != "AAAA" { + t.Fatalf("expected recordType AAAA, got %#v", captured["recordType"]) + } + if captured["ipv6Address"] != "2001:db8::123" { + t.Fatalf("expected ipv6Address in payload, got %#v", captured) + } + if _, ok := captured["content"]; ok { + t.Fatalf("expected content to be omitted for AAAA record, got %#v", captured["content"]) + } + if _, ok := captured["ipv4Address"]; ok { + t.Fatalf("expected ipv4Address to be omitted for AAAA record, got %#v", captured["ipv4Address"]) + } +} + +func TestClientCreateDNSRecordSendsHostForCNAMERecord(t *testing.T) { + var captured map[string]any + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost || r.URL.Path != "/dns/1001/record" { + t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path) + } + if err := json.NewDecoder(r.Body).Decode(&captured); err != nil { + t.Fatalf("failed to decode payload: %v", err) + } + _ = json.NewEncoder(w).Encode(map[string]any{ + "statusCode": 200, + "id": 11, + "domainId": 1001, + "domainName": "example.com", + "nodeName": "alias", + "hostname": "alias.example.com", + "recordType": "CNAME", + "content": "target.example.com", + "host": "target.example.com", + "ttl": 90, + "state": true, + }) + })) + defer server.Close() + + client := dynuclient.New("test-key", dynuclient.WithBaseURL(server.URL), dynuclient.WithHTTPClient(server.Client())) + _, err := client.CreateDNSRecord(context.Background(), 1001, dynuclient.CreateDNSRecordRequest{ + NodeName: "alias", + RecordType: "CNAME", + Content: stringPointer("target.example.com"), + TTL: 90, + }) + if err != nil { + t.Fatalf("CreateDNSRecord() error = %v", err) + } + + if captured["host"] != "target.example.com" { + t.Fatalf("expected host in payload, got %#v", captured) + } + if _, ok := captured["content"]; ok { + t.Fatalf("expected content to be omitted for CNAME record, got %#v", captured["content"]) + } +} + +func TestClientCreateDNSRecordSendsContentForTXTRecord(t *testing.T) { + var captured map[string]any + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost || r.URL.Path != "/dns/1001/record" { + t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path) + } + if err := json.NewDecoder(r.Body).Decode(&captured); err != nil { + t.Fatalf("failed to decode payload: %v", err) + } + _ = json.NewEncoder(w).Encode(map[string]any{ + "statusCode": 200, + "id": 12, + "domainId": 1001, + "domainName": "example.com", + "nodeName": "txt", + "hostname": "txt.example.com", + "recordType": "TXT", + "content": "hello", + "ttl": 90, + "state": true, + }) + })) + defer server.Close() + + client := dynuclient.New("test-key", dynuclient.WithBaseURL(server.URL), dynuclient.WithHTTPClient(server.Client())) + _, err := client.CreateDNSRecord(context.Background(), 1001, dynuclient.CreateDNSRecordRequest{ + NodeName: "txt", + RecordType: "TXT", + Content: stringPointer("hello"), + TTL: 90, + }) + if err != nil { + t.Fatalf("CreateDNSRecord() error = %v", err) + } + + if captured["content"] != "hello" { + t.Fatalf("expected content in payload, got %#v", captured) + } + if _, ok := captured["ipv4Address"]; ok { + t.Fatalf("expected ipv4Address to be omitted for TXT record, got %#v", captured["ipv4Address"]) + } + if _, ok := captured["ipv6Address"]; ok { + t.Fatalf("expected ipv6Address to be omitted for TXT record, got %#v", captured["ipv6Address"]) + } } func TestClientCreateDNSRecordNormalizesZoneStyleContent(t *testing.T) { diff --git a/internal/testutil/fakedynu/server.go b/internal/testutil/fakedynu/server.go index 6bab80c..008f5e7 100644 --- a/internal/testutil/fakedynu/server.go +++ b/internal/testutil/fakedynu/server.go @@ -50,13 +50,15 @@ type rawResponse struct { } type dnsRecordUpsertRequest struct { - NodeName string `json:"nodeName"` - RecordType string `json:"recordType"` - Content string `json:"content"` - TTL int64 `json:"ttl"` - State *bool `json:"state"` - Group string `json:"group"` - Host string `json:"host"` + NodeName string `json:"nodeName"` + RecordType string `json:"recordType"` + Content string `json:"content"` + IPv4Address string `json:"ipv4Address"` + IPv6Address string `json:"ipv6Address"` + TTL int64 `json:"ttl"` + State *bool `json:"state"` + Group string `json:"group"` + Host string `json:"host"` } func NewServer() *Server { @@ -272,7 +274,8 @@ func (s *Server) handleCreateRecord(w http.ResponseWriter, r *http.Request, doma s.writeAPIError(w, APIError{HTTPStatus: http.StatusBadRequest, StatusCode: 400, Type: "Validation Exception", Message: "recordType is required"}) return } - if req.Content == "" && !strings.EqualFold(req.RecordType, "A") && !strings.EqualFold(req.RecordType, "AAAA") { + content := contentFromUpsertRequest(req) + if content == "" && !strings.EqualFold(req.RecordType, "A") && !strings.EqualFold(req.RecordType, "AAAA") { s.writeAPIError(w, APIError{HTTPStatus: http.StatusBadRequest, StatusCode: 400, Type: "Validation Exception", Message: "recordType and content are required"}) return } @@ -299,7 +302,7 @@ func (s *Server) handleCreateRecord(w http.ResponseWriter, r *http.Request, doma RecordType: req.RecordType, State: state, TTL: ttl, - Content: req.Content, + Content: content, UpdatedOn: time.Now().UTC().Format(time.RFC3339), Group: req.Group, Host: req.Host, @@ -320,9 +323,11 @@ func (s *Server) handleUpdateRecord(w http.ResponseWriter, r *http.Request, doma return } + content := contentFromUpsertRequest(req) + record.NodeName = req.NodeName record.RecordType = req.RecordType - record.Content = req.Content + record.Content = content if req.TTL > 0 { record.TTL = req.TTL } @@ -338,6 +343,20 @@ func (s *Server) handleUpdateRecord(w http.ResponseWriter, r *http.Request, doma s.writeRecord(w, record) } +func contentFromUpsertRequest(req dnsRecordUpsertRequest) string { + switch strings.ToUpper(strings.TrimSpace(req.RecordType)) { + case "A": + return strings.TrimSpace(req.IPv4Address) + case "AAAA": + return strings.TrimSpace(req.IPv6Address) + case "CNAME": + if strings.TrimSpace(req.Host) != "" { + return strings.TrimSpace(req.Host) + } + } + return strings.TrimSpace(req.Content) +} + func (s *Server) handleDeleteRecord(w http.ResponseWriter, domainID int64, recordID int64) { _, idx, ok := s.findRecord(domainID, recordID) if !ok {