diff --git a/clerk.go b/clerk.go index a6f4b2fa..87009a91 100644 --- a/clerk.go +++ b/clerk.go @@ -258,7 +258,7 @@ func (b *defaultBackend) do(req *http.Request, params Queryable, setter Response func setRequestBody(req *http.Request, params Queryable) error { // GET requests don't have a body, but we will pass the params // in the query string. - if req.Method == http.MethodGet { + if req.Method == http.MethodGet && params != nil { q := req.URL.Query() params.Add(q) req.URL.RawQuery = q.Encode() diff --git a/deleted_resource.go b/deleted_resource.go new file mode 100644 index 00000000..43a21899 --- /dev/null +++ b/deleted_resource.go @@ -0,0 +1,12 @@ +package clerk + +// DeletedResource describes an API resource that is no longer +// available. +// It's usually encountered as a result of delete API operations. +type DeletedResource struct { + APIResource + ID string `json:"id,omitempty"` + Slug string `json:"slug,omitempty"` + Object string `json:"object"` + Deleted bool `json:"deleted"` +} diff --git a/domain.go b/domain.go new file mode 100644 index 00000000..21366b96 --- /dev/null +++ b/domain.go @@ -0,0 +1,25 @@ +package clerk + +type Domain struct { + APIResource + ID string `json:"id"` + Object string `json:"object"` + Name string `json:"name"` + IsSatellite bool `json:"is_satellite"` + FrontendAPIURL string `json:"frontend_api_url"` + AccountPortalURL *string `json:"accounts_portal_url,omitempty"` + ProxyURL *string `json:"proxy_url,omitempty"` + CNAMETargets []CNAMETarget `json:"cname_targets,omitempty"` + DevelopmentOrigin string `json:"development_origin"` +} + +type CNAMETarget struct { + Host string `json:"host"` + Value string `json:"value"` +} + +type DomainList struct { + APIResource + Domains []*Domain `json:"data"` + TotalCount int64 `json:"total_count"` +} diff --git a/domain/domain.go b/domain/domain.go new file mode 100644 index 00000000..717a7faf --- /dev/null +++ b/domain/domain.go @@ -0,0 +1,73 @@ +// Package domain provides the Domains API. +package domain + +import ( + "context" + "net/http" + "net/url" + + "github.com/clerk/clerk-sdk-go/v2" +) + +const path = "/domains" + +type CreateParams struct { + clerk.APIParams + Name *string `json:"name,omitempty"` + ProxyURL *string `json:"proxy_url,omitempty"` + IsSatellite *bool `json:"is_satellite,omitempty"` +} + +// Create creates a new domain. +func Create(ctx context.Context, params *CreateParams) (*clerk.Domain, error) { + req := clerk.NewAPIRequest(http.MethodPost, path) + req.SetParams(params) + + domain := &clerk.Domain{} + err := clerk.GetBackend().Call(ctx, req, domain) + return domain, err +} + +type UpdateParams struct { + clerk.APIParams + Name *string `json:"name,omitempty"` + ProxyURL *string `json:"proxy_url,omitempty"` +} + +// Update updates a domain's properties. +func Update(ctx context.Context, id string, params *UpdateParams) (*clerk.Domain, error) { + path, err := url.JoinPath(path, id) + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodPatch, path) + req.SetParams(params) + + domain := &clerk.Domain{} + err = clerk.GetBackend().Call(ctx, req, domain) + return domain, err +} + +// Delete removes a domain. +func Delete(ctx context.Context, id string) (*clerk.DeletedResource, error) { + path, err := url.JoinPath(path, id) + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodDelete, path) + domain := &clerk.DeletedResource{} + err = clerk.GetBackend().Call(ctx, req, domain) + return domain, err +} + +type ListParams struct { + clerk.APIParams +} + +// List returns a list of domains +func List(ctx context.Context, params *ListParams) (*clerk.DomainList, error) { + req := clerk.NewAPIRequest(http.MethodGet, path) + list := &clerk.DomainList{} + err := clerk.GetBackend().Call(ctx, req, list) + return list, err +} diff --git a/domain/domain_test.go b/domain/domain_test.go new file mode 100644 index 00000000..d0540545 --- /dev/null +++ b/domain/domain_test.go @@ -0,0 +1,194 @@ +package domain + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "testing" + + "github.com/clerk/clerk-sdk-go/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDomainCreate(t *testing.T) { + name := "clerk.com" + id := "dmn_123" + clerk.SetBackend(clerk.NewBackend(&clerk.BackendConfig{ + HTTPClient: &http.Client{ + Transport: &mockRoundTripper{ + T: t, + in: json.RawMessage(fmt.Sprintf(`{"name":"%s"}`, name)), + out: json.RawMessage(fmt.Sprintf(`{"id":"%s","name":"%s"}`, id, name)), + path: "/v1/domains", + method: http.MethodPost, + }, + }, + })) + + dmn, err := Create(context.Background(), &CreateParams{ + Name: clerk.String(name), + }) + require.NoError(t, err) + assert.Equal(t, id, dmn.ID) + assert.Equal(t, name, dmn.Name) +} + +func TestDomainCreate_Error(t *testing.T) { + clerk.SetBackend(clerk.NewBackend(&clerk.BackendConfig{ + HTTPClient: &http.Client{ + Transport: &mockRoundTripper{ + T: t, + status: http.StatusBadRequest, + out: json.RawMessage(`{ + "errors":[{ + "code":"create-error-code" + }], + "clerk_trace_id":"create-trace-id" +}`), + }, + }, + })) + + _, err := Create(context.Background(), &CreateParams{}) + require.Error(t, err) + apiErr, ok := err.(*clerk.APIErrorResponse) + require.True(t, ok) + assert.Equal(t, "create-trace-id", apiErr.TraceID) + require.Equal(t, 1, len(apiErr.Errors)) + assert.Equal(t, "create-error-code", apiErr.Errors[0].Code) +} + +func TestDomainUpdate(t *testing.T) { + id := "dmn_456" + name := "clerk.dev" + clerk.SetBackend(clerk.NewBackend(&clerk.BackendConfig{ + HTTPClient: &http.Client{ + Transport: &mockRoundTripper{ + T: t, + in: json.RawMessage(fmt.Sprintf(`{"name":"%s"}`, name)), + out: json.RawMessage(fmt.Sprintf(`{"id":"%s","name":"%s"}`, id, name)), + path: fmt.Sprintf("/v1/domains/%s", id), + method: http.MethodPatch, + }, + }, + })) + + dmn, err := Update(context.Background(), id, &UpdateParams{ + Name: clerk.String(name), + }) + require.NoError(t, err) + assert.Equal(t, id, dmn.ID) + assert.Equal(t, name, dmn.Name) +} + +func TestDomainUpdate_Error(t *testing.T) { + clerk.SetBackend(clerk.NewBackend(&clerk.BackendConfig{ + HTTPClient: &http.Client{ + Transport: &mockRoundTripper{ + T: t, + status: http.StatusBadRequest, + out: json.RawMessage(`{ + "errors":[{ + "code":"update-error-code" + }], + "clerk_trace_id":"update-trace-id" +}`), + }, + }, + })) + + _, err := Update(context.Background(), "dmn_123", &UpdateParams{}) + require.Error(t, err) + apiErr, ok := err.(*clerk.APIErrorResponse) + require.True(t, ok) + assert.Equal(t, "update-trace-id", apiErr.TraceID) + require.Equal(t, 1, len(apiErr.Errors)) + assert.Equal(t, "update-error-code", apiErr.Errors[0].Code) +} + +func TestDomainDelete(t *testing.T) { + id := "dmn_789" + clerk.SetBackend(clerk.NewBackend(&clerk.BackendConfig{ + HTTPClient: &http.Client{ + Transport: &mockRoundTripper{ + T: t, + out: json.RawMessage(fmt.Sprintf(`{"id":"%s","deleted":true}`, id)), + path: fmt.Sprintf("/v1/domains/%s", id), + method: http.MethodDelete, + }, + }, + })) + + dmn, err := Delete(context.Background(), id) + require.NoError(t, err) + assert.Equal(t, id, dmn.ID) + assert.True(t, dmn.Deleted) +} + +func TestDomainList(t *testing.T) { + clerk.SetBackend(clerk.NewBackend(&clerk.BackendConfig{ + HTTPClient: &http.Client{ + Transport: &mockRoundTripper{ + T: t, + out: json.RawMessage(`{ + "data": [{"id":"dmn_123","name":"clerk.com"}], + "total_count": 1 +}`), + path: "/v1/domains", + method: http.MethodGet, + }, + }, + })) + + list, err := List(context.Background(), &ListParams{}) + require.NoError(t, err) + assert.Equal(t, int64(1), list.TotalCount) + assert.Equal(t, 1, len(list.Domains)) + assert.Equal(t, "dmn_123", list.Domains[0].ID) + assert.Equal(t, "clerk.com", list.Domains[0].Name) +} + +type mockRoundTripper struct { + T *testing.T + // Response status. + status int + // Response body. + out json.RawMessage + // If set, we'll assert that the request body + // matches. + in json.RawMessage + // If set, we'll assert the request path matches. + path string + // If set, we'll assert that the request method matches. + method string +} + +func (rt *mockRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { + if rt.status == 0 { + rt.status = http.StatusOK + } + + if rt.method != "" { + require.Equal(rt.T, rt.method, r.Method) + } + if rt.path != "" { + require.Equal(rt.T, rt.path, r.URL.Path) + } + if rt.in != nil { + body, err := io.ReadAll(r.Body) + if err != nil { + return nil, err + } + defer r.Body.Close() + require.JSONEq(rt.T, string(rt.in), string(body)) + } + + return &http.Response{ + StatusCode: rt.status, + Body: io.NopCloser(bytes.NewReader(rt.out)), + }, nil +}