Archived
Harden read-only provider with tests, docs, and CI
This commit is contained in:
1 parent
cf47a6ed48
commit
6cc1b206dc
24 files changed
+711
-246
No files matched your search
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -136,7 +137,7 @@ func (c *Client) GetDomainByID(ctx context.Context, domainID int64) (*Domain, er
|
||||
|
||||
func (c *Client) GetRootDomain(ctx context.Context, hostname string) (int64, string, error) {
|
||||
var resp getRootResponse
|
||||
if err := c.doGET(ctx, fmt.Sprintf("/dns/getroot/%s", hostname), &resp); err != nil {
|
||||
if err := c.doGET(ctx, fmt.Sprintf("/dns/getroot/%s", url.PathEscape(hostname)), &resp); err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
@@ -156,8 +157,8 @@ func (c *Client) ListDNSRecords(ctx context.Context, domainID int64) ([]DNSRecor
|
||||
}
|
||||
|
||||
func (c *Client) doGET(ctx context.Context, path string, target any) error {
|
||||
url := c.baseURL + path
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
requestURL := c.baseURL + path
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -176,7 +177,11 @@ func (c *Client) doGET(ctx context.Context, path string, target any) error {
|
||||
return err
|
||||
}
|
||||
|
||||
apiErr := parseAPIException(payload)
|
||||
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
||||
if apiErr != nil {
|
||||
return apiErr
|
||||
}
|
||||
return fmt.Errorf("dynu API returned status %d: %s", res.StatusCode, strings.TrimSpace(string(payload)))
|
||||
}
|
||||
|
||||
@@ -184,10 +189,18 @@ func (c *Client) doGET(ctx context.Context, path string, target any) error {
|
||||
return fmt.Errorf("failed to decode dynu API response: %w", err)
|
||||
}
|
||||
|
||||
apiResult := apiResponse{}
|
||||
if err := json.Unmarshal(payload, &apiResult); err == nil && apiResult.Exception != nil {
|
||||
return fmt.Errorf("dynu API error %d (%s): %s", apiResult.Exception.StatusCode, apiResult.Exception.Type, apiResult.Exception.Message)
|
||||
if apiErr != nil {
|
||||
return apiErr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseAPIException(payload []byte) error {
|
||||
apiResult := apiResponse{}
|
||||
if err := json.Unmarshal(payload, &apiResult); err != nil || apiResult.Exception == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("dynu API error %d (%s): %s", apiResult.Exception.StatusCode, apiResult.Exception.Type, apiResult.Exception.Message)
|
||||
}
|
||||
@@ -4,10 +4,11 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestListDomains(t *testing.T) {
|
||||
func TestClientListDomainsSuccess(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/dns" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
@@ -16,7 +17,7 @@ func TestListDomains(t *testing.T) {
|
||||
t.Fatalf("unexpected api key header: %s", got)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"statusCode":200,"domains":[{"id":1,"name":"example.com","state":"Complete"}]}`))
|
||||
_, _ = w.Write([]byte(`{"statusCode":200,"domains":[{"id":2,"name":"z.example.com"},{"id":1,"name":"a.example.com"}]}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
@@ -25,24 +26,79 @@ func TestListDomains(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("ListDomains() error = %v", err)
|
||||
}
|
||||
if len(domains) != 1 {
|
||||
t.Fatalf("expected 1 domain, got %d", len(domains))
|
||||
}
|
||||
if domains[0].Name != "example.com" {
|
||||
t.Fatalf("unexpected domain name %q", domains[0].Name)
|
||||
if len(domains) != 2 {
|
||||
t.Fatalf("expected 2 domains, got %d", len(domains))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoGetErrorResponse(t *testing.T) {
|
||||
func TestClientDoGetNon2xxStatus(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_, _ = w.Write([]byte(`{"statusCode":401,"exception":{"statusCode":401,"type":"Authentication Exception","message":"invalid"}}`))
|
||||
_, _ = w.Write([]byte(`{"message":"nope"}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := New("test-key", WithBaseURL(ts.URL), WithHTTPClient(ts.Client()))
|
||||
_, err := client.ListDomains(context.Background())
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
if err == nil || !strings.Contains(err.Error(), "status 401") {
|
||||
t.Fatalf("expected status error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientDoGetAPIExceptionPayload(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"statusCode":200,"exception":{"statusCode":400,"type":"Validation Exception","message":"bad hostname"}}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := New("test-key", WithBaseURL(ts.URL), WithHTTPClient(ts.Client()))
|
||||
_, err := client.ListDomains(context.Background())
|
||||
if err == nil || !strings.Contains(err.Error(), "Validation Exception") {
|
||||
t.Fatalf("expected API exception error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientDoGetMalformedJSON(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"statusCode":200,"domains":[`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := New("test-key", WithBaseURL(ts.URL), WithHTTPClient(ts.Client()))
|
||||
_, err := client.ListDomains(context.Background())
|
||||
if err == nil || !strings.Contains(err.Error(), "failed to decode dynu API response") {
|
||||
t.Fatalf("expected decode error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientGetRootDomainIncompleteResponse(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"statusCode":200,"id":0,"domainName":""}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := New("test-key", WithBaseURL(ts.URL), WithHTTPClient(ts.Client()))
|
||||
_, _, err := client.GetRootDomain(context.Background(), "www.example.com")
|
||||
if err == nil || !strings.Contains(err.Error(), "incomplete root domain response") {
|
||||
t.Fatalf("expected incomplete response error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientGetRootDomainEscapesHostname(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.Contains(r.RequestURI, "spaces%20in%20name.example.com") {
|
||||
t.Fatalf("expected escaped hostname path, got %s", r.RequestURI)
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"statusCode":200,"id":123,"domainName":"example.com"}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := New("test-key", WithBaseURL(ts.URL), WithHTTPClient(ts.Client()))
|
||||
_, _, err := client.GetRootDomain(context.Background(), "spaces in name.example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error, got %v", err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user