diff --git a/internal/dynuclient/client.go b/internal/dynuclient/client.go index 7688a62..0b80c72 100644 --- a/internal/dynuclient/client.go +++ b/internal/dynuclient/client.go @@ -202,7 +202,7 @@ func (c *Client) GetDNSRecord(ctx context.Context, domainID int64, recordID int6 func (c *Client) CreateDNSRecord(ctx context.Context, domainID int64, req CreateDNSRecordRequest) (*DNSRecord, error) { var resp getDNSRecordResponse - if err := c.doRequest(ctx, http.MethodPost, fmt.Sprintf("/dns/%d/record", domainID), req, &resp); err != nil { + if err := c.doRequest(ctx, http.MethodPost, fmt.Sprintf("/dns/%d/record", domainID), buildDNSRecordUpsertPayload(req.RecordType, req.NodeName, req.Content, req.TTL, req.State, req.Group, req.Host), &resp); err != nil { return nil, err } return &resp.DNSRecord, nil @@ -210,7 +210,7 @@ func (c *Client) CreateDNSRecord(ctx context.Context, domainID int64, req Create func (c *Client) UpdateDNSRecord(ctx context.Context, domainID int64, recordID int64, req UpdateDNSRecordRequest) (*DNSRecord, error) { var resp getDNSRecordResponse - if err := c.doRequest(ctx, http.MethodPut, fmt.Sprintf("/dns/%d/record/%d", domainID, recordID), req, &resp); err != nil { + if err := c.doRequest(ctx, http.MethodPut, fmt.Sprintf("/dns/%d/record/%d", domainID, recordID), buildDNSRecordUpsertPayload(req.RecordType, req.NodeName, req.Content, req.TTL, req.State, req.Group, req.Host), &resp); err != nil { return nil, err } return &resp.DNSRecord, nil @@ -277,13 +277,64 @@ func (c *Client) doRequest(ctx context.Context, method string, path string, requ func parseAPIException(payload []byte) error { apiResult := apiResponse{} - if err := json.Unmarshal(payload, &apiResult); err != nil || apiResult.Exception == nil { + if err := json.Unmarshal(payload, &apiResult); err != nil { + return nil + } + + if apiResult.Exception != nil { + return &APIError{ + StatusCode: apiResult.Exception.StatusCode, + Type: apiResult.Exception.Type, + Message: apiResult.Exception.Message, + } + } + + // Some Dynu responses surface API failures at the top level instead of under exception. + topLevel := apiException{} + if err := json.Unmarshal(payload, &topLevel); err != nil || topLevel.StatusCode == 0 || topLevel.Type == "" { return nil } return &APIError{ - StatusCode: apiResult.Exception.StatusCode, - Type: apiResult.Exception.Type, - Message: apiResult.Exception.Message, + StatusCode: topLevel.StatusCode, + Type: topLevel.Type, + Message: topLevel.Message, } } + +type dnsRecordUpsertPayload struct { + NodeName string `json:"nodeName,omitempty"` + RecordType string `json:"recordType"` + Content string `json:"content,omitempty"` + IPv4Address string `json:"ipv4Address,omitempty"` + IPv6Address string `json:"ipv6Address,omitempty"` + TTL int64 `json:"ttl,omitempty"` + State *bool `json:"state,omitempty"` + Group string `json:"group,omitempty"` + Host string `json:"host,omitempty"` +} + +func buildDNSRecordUpsertPayload(recordType string, nodeName string, content string, ttl int64, state *bool, group string, host string) dnsRecordUpsertPayload { + payload := dnsRecordUpsertPayload{ + NodeName: nodeName, + RecordType: recordType, + Content: content, + TTL: ttl, + State: state, + Group: group, + Host: host, + } + + switch strings.ToUpper(strings.TrimSpace(recordType)) { + case "A": + payload.IPv4Address = content + case "AAAA": + payload.IPv6Address = content + case "CNAME": + if payload.Host == "" { + payload.Host = content + } + } + + return payload +} diff --git a/internal/dynuclient/client_test.go b/internal/dynuclient/client_test.go index e599e63..7074e67 100644 --- a/internal/dynuclient/client_test.go +++ b/internal/dynuclient/client_test.go @@ -2,6 +2,9 @@ package dynuclient_test import ( "context" + "encoding/json" + "net/http" + "net/http/httptest" "strings" "testing" @@ -147,3 +150,55 @@ func TestClientDNSRecordWriteAPIError(t *testing.T) { t.Fatalf("expected validation API error, got %v", err) } } + +func TestClientCreateDNSRecordSendsIPv4AddressForARecord(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": 10, + "domainId": 1001, + "domainName": "example.com", + "nodeName": "www", + "hostname": "www.example.com", + "recordType": "A", + "content": "167.179.167.166", + "ttl": 300, + "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: "www", + RecordType: "A", + Content: "167.179.167.166", + TTL: 300, + }) + if err != nil { + t.Fatalf("CreateDNSRecord() error = %v", err) + } + + if captured["ipv4Address"] != "167.179.167.166" { + t.Fatalf("expected ipv4Address in payload, got %#v", captured) + } +} + +func TestClientDoRequestTopLevelAPIExceptionPayload(t *testing.T) { + fake := fakedynu.NewServer() + defer fake.Close() + fake.SetRawResponse("/dns", 505, `{"statusCode":505,"type":"Validation Exception","message":"Invalid IP address."}`) + + client := dynuclient.New("test-key", dynuclient.WithBaseURL(fake.BaseURL()), dynuclient.WithHTTPClient(fake.Client())) + _, err := client.ListDomains(context.Background()) + if err == nil || !strings.Contains(err.Error(), "Invalid IP address.") { + t.Fatalf("expected top-level API error, got %v", err) + } +}