diff --git a/actor_token.go b/actor_token.go new file mode 100644 index 00000000..91fffb1f --- /dev/null +++ b/actor_token.go @@ -0,0 +1,15 @@ +package clerk + +import "encoding/json" + +type ActorToken struct { + APIResource + Object string `json:"object"` + ID string `json:"id"` + UserID string `json:"user_id"` + Actor json.RawMessage `json:"actor"` + Token string `json:"token,omitempty"` + Status string `json:"status"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} diff --git a/actortoken/actor_token.go b/actortoken/actor_token.go new file mode 100644 index 00000000..365fa8cd --- /dev/null +++ b/actortoken/actor_token.go @@ -0,0 +1,42 @@ +// Package actortoken provides the Actor Tokens API. +package actortoken + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + + "github.com/clerk/clerk-sdk-go/v2" +) + +const path = "/actor_tokens" + +type CreateParams struct { + clerk.APIParams + UserID *string `json:"user_id,omitempty"` + Actor json.RawMessage `json:"actor,omitempty"` + ExpiresInSeconds *int64 `json:"expires_in_seconds,omitempty"` + SessionMaxDurationInSeconds *int64 `json:"session_max_duration_in_seconds,omitempty"` +} + +// Create creates a new actor token. +func Create(ctx context.Context, params *CreateParams) (*clerk.ActorToken, error) { + req := clerk.NewAPIRequest(http.MethodPost, path) + req.SetParams(params) + token := &clerk.ActorToken{} + err := clerk.GetBackend().Call(ctx, req, token) + return token, err +} + +// Revoke revokes a pending actor token. +func Revoke(ctx context.Context, id string) (*clerk.ActorToken, error) { + path, err := url.JoinPath(path, id, "revoke") + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodPost, path) + token := &clerk.ActorToken{} + err = clerk.GetBackend().Call(ctx, req, token) + return token, err +} diff --git a/actortoken/actor_token_test.go b/actortoken/actor_token_test.go new file mode 100644 index 00000000..614c6c68 --- /dev/null +++ b/actortoken/actor_token_test.go @@ -0,0 +1,80 @@ +package actortoken + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/clerk/clerk-sdk-go/v2" + "github.com/clerk/clerk-sdk-go/v2/clerktest" + "github.com/stretchr/testify/require" +) + +func TestActorTokenCreate(t *testing.T) { + userID := "usr_123" + id := "act_123" + clerk.SetBackend(clerk.NewBackend(&clerk.BackendConfig{ + HTTPClient: &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + In: json.RawMessage(fmt.Sprintf(`{"user_id":"%s"}`, userID)), + Out: json.RawMessage(fmt.Sprintf(`{"id":"%s","user_id":"%s"}`, id, userID)), + Path: "/v1/actor_tokens", + Method: http.MethodPost, + }, + }, + })) + + actorToken, err := Create(context.Background(), &CreateParams{ + UserID: clerk.String(userID), + }) + require.NoError(t, err) + require.Equal(t, id, actorToken.ID) + require.Equal(t, userID, actorToken.UserID) +} + +func TestActorTokenCreate_Error(t *testing.T) { + clerk.SetBackend(clerk.NewBackend(&clerk.BackendConfig{ + HTTPClient: &http.Client{ + Transport: &clerktest.RoundTripper{ + 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) + require.Equal(t, "create-trace-id", apiErr.TraceID) + require.Equal(t, 1, len(apiErr.Errors)) + require.Equal(t, "create-error-code", apiErr.Errors[0].Code) +} + +func TestActorTokenRevoke(t *testing.T) { + id := "act_456" + clerk.SetBackend(clerk.NewBackend(&clerk.BackendConfig{ + HTTPClient: &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(fmt.Sprintf(`{"id":"%s","status":"revoked"}`, id)), + Path: fmt.Sprintf("/v1/actor_tokens/%s/revoke", id), + Method: http.MethodPost, + }, + }, + })) + + actorToken, err := Revoke(context.Background(), id) + require.NoError(t, err) + require.Equal(t, id, actorToken.ID) + require.Equal(t, "revoked", actorToken.Status) +} diff --git a/clerktest/clerktest.go b/clerktest/clerktest.go new file mode 100644 index 00000000..cec403be --- /dev/null +++ b/clerktest/clerktest.go @@ -0,0 +1,55 @@ +// Package clerktest provides utilities for testing. +package clerktest + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/require" +) + +// RoundTripper can be used as a mock Transport for http.Clients. +// Set the RoundTripper's fields accordingly to determine the +// response or perform assertions on the http.Request properties. +type RoundTripper struct { + T *testing.T + // Status is the response Status code. + Status int + // Out is the response body. + Out json.RawMessage + // Set this field to assert on the request method. + Method string + // Set this field to assert that the request path matches. + Path string + // Set this field to assert that the request body matches. + In json.RawMessage +} + +// RoundTrip returns an http.Response based on the RoundTripper's fields. +// It will also perform assertions on the http.Request. +func (rt *RoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { + if rt.Status == 0 { + rt.Status = http.StatusOK + } + if rt.Path != "" { + require.Equal(rt.T, rt.Path, r.URL.Path) + } + if rt.Method != "" { + require.Equal(rt.T, rt.Method, r.Method) + } + 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 +} diff --git a/domain/domain_test.go b/domain/domain_test.go index d0540545..782c89fd 100644 --- a/domain/domain_test.go +++ b/domain/domain_test.go @@ -1,16 +1,14 @@ 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/clerk/clerk-sdk-go/v2/clerktest" "github.com/stretchr/testify/require" ) @@ -19,12 +17,12 @@ func TestDomainCreate(t *testing.T) { id := "dmn_123" clerk.SetBackend(clerk.NewBackend(&clerk.BackendConfig{ HTTPClient: &http.Client{ - Transport: &mockRoundTripper{ + Transport: &clerktest.RoundTripper{ 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, + 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, }, }, })) @@ -33,17 +31,17 @@ func TestDomainCreate(t *testing.T) { Name: clerk.String(name), }) require.NoError(t, err) - assert.Equal(t, id, dmn.ID) - assert.Equal(t, name, dmn.Name) + require.Equal(t, id, dmn.ID) + require.Equal(t, name, dmn.Name) } func TestDomainCreate_Error(t *testing.T) { clerk.SetBackend(clerk.NewBackend(&clerk.BackendConfig{ HTTPClient: &http.Client{ - Transport: &mockRoundTripper{ + Transport: &clerktest.RoundTripper{ T: t, - status: http.StatusBadRequest, - out: json.RawMessage(`{ + Status: http.StatusBadRequest, + Out: json.RawMessage(`{ "errors":[{ "code":"create-error-code" }], @@ -57,9 +55,9 @@ func TestDomainCreate_Error(t *testing.T) { require.Error(t, err) apiErr, ok := err.(*clerk.APIErrorResponse) require.True(t, ok) - assert.Equal(t, "create-trace-id", apiErr.TraceID) + require.Equal(t, "create-trace-id", apiErr.TraceID) require.Equal(t, 1, len(apiErr.Errors)) - assert.Equal(t, "create-error-code", apiErr.Errors[0].Code) + require.Equal(t, "create-error-code", apiErr.Errors[0].Code) } func TestDomainUpdate(t *testing.T) { @@ -67,12 +65,12 @@ func TestDomainUpdate(t *testing.T) { name := "clerk.dev" clerk.SetBackend(clerk.NewBackend(&clerk.BackendConfig{ HTTPClient: &http.Client{ - Transport: &mockRoundTripper{ + Transport: &clerktest.RoundTripper{ 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, + 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, }, }, })) @@ -81,17 +79,17 @@ func TestDomainUpdate(t *testing.T) { Name: clerk.String(name), }) require.NoError(t, err) - assert.Equal(t, id, dmn.ID) - assert.Equal(t, name, dmn.Name) + require.Equal(t, id, dmn.ID) + require.Equal(t, name, dmn.Name) } func TestDomainUpdate_Error(t *testing.T) { clerk.SetBackend(clerk.NewBackend(&clerk.BackendConfig{ HTTPClient: &http.Client{ - Transport: &mockRoundTripper{ + Transport: &clerktest.RoundTripper{ T: t, - status: http.StatusBadRequest, - out: json.RawMessage(`{ + Status: http.StatusBadRequest, + Out: json.RawMessage(`{ "errors":[{ "code":"update-error-code" }], @@ -105,90 +103,49 @@ func TestDomainUpdate_Error(t *testing.T) { require.Error(t, err) apiErr, ok := err.(*clerk.APIErrorResponse) require.True(t, ok) - assert.Equal(t, "update-trace-id", apiErr.TraceID) + require.Equal(t, "update-trace-id", apiErr.TraceID) require.Equal(t, 1, len(apiErr.Errors)) - assert.Equal(t, "update-error-code", apiErr.Errors[0].Code) + require.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{ + Transport: &clerktest.RoundTripper{ T: t, - out: json.RawMessage(fmt.Sprintf(`{"id":"%s","deleted":true}`, id)), - path: fmt.Sprintf("/v1/domains/%s", id), - method: http.MethodDelete, + 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) + require.Equal(t, id, dmn.ID) + require.True(t, dmn.Deleted) } func TestDomainList(t *testing.T) { clerk.SetBackend(clerk.NewBackend(&clerk.BackendConfig{ HTTPClient: &http.Client{ - Transport: &mockRoundTripper{ + Transport: &clerktest.RoundTripper{ T: t, - out: json.RawMessage(`{ + Out: json.RawMessage(`{ "data": [{"id":"dmn_123","name":"clerk.com"}], "total_count": 1 }`), - path: "/v1/domains", - method: http.MethodGet, + 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 + require.Equal(t, int64(1), list.TotalCount) + require.Equal(t, 1, len(list.Domains)) + require.Equal(t, "dmn_123", list.Domains[0].ID) + require.Equal(t, "clerk.com", list.Domains[0].Name) }