diff --git a/clerk.go b/clerk.go index 1d48428f..4b91a47f 100644 --- a/clerk.go +++ b/clerk.go @@ -256,9 +256,11 @@ func (b *defaultBackend) do(req *http.Request, params Params, setter ResponseRea } setter.Read(apiResponse) - err = json.Unmarshal(resBody, setter) - if err != nil { - return err + if len(resBody) > 0 { + err := json.Unmarshal(resBody, setter) + if err != nil { + return err + } } return nil diff --git a/oauth_access_token.go b/oauth_access_token.go new file mode 100644 index 00000000..eb5ac05b --- /dev/null +++ b/oauth_access_token.go @@ -0,0 +1,21 @@ +package clerk + +import "encoding/json" + +type OAuthAccessToken struct { + Object string `json:"object"` + Token string `json:"token"` + Provider string `json:"provider"` + PublicMetadata json.RawMessage `json:"public_metadata"` + Label *string `json:"label"` + // Only set in OAuth 2.0 tokens + Scopes []string `json:"scopes,omitempty"` + // Only set in OAuth 1.0 tokens + TokenSecret *string `json:"token_secret,omitempty"` +} + +type OAuthAccessTokenList struct { + APIResource + OAuthAccessTokens []*OAuthAccessToken `json:"data"` + TotalCount int64 `json:"total_count"` +} diff --git a/phone_number.go b/phone_number.go new file mode 100644 index 00000000..8d43bd41 --- /dev/null +++ b/phone_number.go @@ -0,0 +1,14 @@ +package clerk + +type PhoneNumber struct { + APIResource + Object string `json:"object"` + ID string `json:"id"` + PhoneNumber string `json:"phone_number"` + ReservedForSecondFactor bool `json:"reserved_for_second_factor"` + DefaultSecondFactor bool `json:"default_second_factor"` + Reserved bool `json:"reserved"` + Verification *Verification `json:"verification"` + LinkedTo []*LinkedIdentification `json:"linked_to"` + BackupCodes []string `json:"backup_codes"` +} diff --git a/user.go b/user.go new file mode 100644 index 00000000..b2b6b1a9 --- /dev/null +++ b/user.go @@ -0,0 +1,85 @@ +package clerk + +import "encoding/json" + +type User struct { + APIResource + Object string `json:"object"` + ID string `json:"id"` + Username *string `json:"username"` + FirstName *string `json:"first_name"` + LastName *string `json:"last_name"` + ImageURL *string `json:"image_url,omitempty"` + HasImage bool `json:"has_image"` + PrimaryEmailAddressID *string `json:"primary_email_address_id"` + PrimaryPhoneNumberID *string `json:"primary_phone_number_id"` + PrimaryWeb3WalletID *string `json:"primary_web3_wallet_id"` + PasswordEnabled bool `json:"password_enabled"` + TwoFactorEnabled bool `json:"two_factor_enabled"` + TOTPEnabled bool `json:"totp_enabled"` + BackupCodeEnabled bool `json:"backup_code_enabled"` + EmailAddresses []*EmailAddress `json:"email_addresses"` + PhoneNumbers []*PhoneNumber `json:"phone_numbers"` + Web3Wallets []*Web3Wallet `json:"web3_wallets"` + ExternalAccounts []*ExternalAccount `json:"external_accounts"` + SAMLAccounts []*SAMLAccount `json:"saml_accounts"` + PasswordLastUpdatedAt *int64 `json:"password_last_updated_at,omitempty"` + PublicMetadata json.RawMessage `json:"public_metadata"` + PrivateMetadata json.RawMessage `json:"private_metadata,omitempty"` + UnsafeMetadata json.RawMessage `json:"unsafe_metadata,omitempty"` + ExternalID *string `json:"external_id"` + LastSignInAt *int64 `json:"last_sign_in_at"` + Banned bool `json:"banned"` + Locked bool `json:"locked"` + LockoutExpiresInSeconds *int64 `json:"lockout_expires_in_seconds"` + VerificationAttemptsRemaining *int64 `json:"verification_attempts_remaining"` + DeleteSelfEnabled bool `json:"delete_self_enabled"` + CreateOrganizationEnabled bool `json:"create_organization_enabled"` + LastActiveAt *int64 `json:"last_active_at"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +type ExternalAccount struct { + Object string `json:"object"` + ID string `json:"id"` + Provider string `json:"provider"` + IdentificationID string `json:"identification_id"` + ProviderUserID string `json:"provider_user_id"` + ApprovedScopes string `json:"approved_scopes"` + EmailAddress string `json:"email_address"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + AvatarURL string `json:"avatar_url"` + ImageURL *string `json:"image_url,omitempty"` + Username *string `json:"username"` + PublicMetadata json.RawMessage `json:"public_metadata"` + Label *string `json:"label"` + Verification *Verification `json:"verification"` +} + +type Web3Wallet struct { + Object string `json:"object"` + ID string `json:"id"` + Web3Wallet string `json:"web3_wallet"` + Verification *Verification `json:"verification"` +} + +type SAMLAccount struct { + Object string `json:"object"` + ID string `json:"id"` + Provider string `json:"provider"` + Active bool `json:"active"` + EmailAddress string `json:"email_address"` + FirstName *string `json:"first_name"` + LastName *string `json:"last_name"` + ProviderUserID *string `json:"provider_user_id"` + PublicMetadata json.RawMessage `json:"public_metadata"` + Verification *Verification `json:"verification"` +} + +type UserList struct { + APIResource + Users []*User `json:"data"` + TotalCount int64 `json:"total_count"` +} diff --git a/user/api.go b/user/api.go new file mode 100644 index 00000000..9b400abb --- /dev/null +++ b/user/api.go @@ -0,0 +1,88 @@ +// Code generated by "gen"; DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. +// Last generated at 2024-02-09 08:26:26.341970834 +0000 UTC +package user + +import ( + "context" + + "github.com/clerk/clerk-sdk-go/v2" +) + +// Create creates a new user. +func Create(ctx context.Context, params *CreateParams) (*clerk.User, error) { + return getClient().Create(ctx, params) +} + +// Get retrieves details about the user. +func Get(ctx context.Context, id string) (*clerk.User, error) { + return getClient().Get(ctx, id) +} + +// Update updates a user. +func Update(ctx context.Context, id string, params *UpdateParams) (*clerk.User, error) { + return getClient().Update(ctx, id, params) +} + +// UpdateMetadata updates the user's metadata by merging the +// provided values with the existing ones. +func UpdateMetadata(ctx context.Context, id string, params *UpdateMetadataParams) (*clerk.User, error) { + return getClient().UpdateMetadata(ctx, id, params) +} + +// Delete deletes a user. +func Delete(ctx context.Context, id string) (*clerk.DeletedResource, error) { + return getClient().Delete(ctx, id) +} + +// List returns a list of users. +func List(ctx context.Context, params *ListParams) (*clerk.UserList, error) { + return getClient().List(ctx, params) +} + +// Count returns the total count of users satisfying the parameters. +func Count(ctx context.Context, params *ListParams) (*TotalCount, error) { + return getClient().Count(ctx, params) +} + +// ListOAuthAccessTokens retrieves a list of the user's access +// tokens for a specific OAuth provider. +func ListOAuthAccessTokens(ctx context.Context, params *ListOAuthAccessTokensParams) (*clerk.OAuthAccessTokenList, error) { + return getClient().ListOAuthAccessTokens(ctx, params) +} + +// DeleteMFA disables a user's multi-factor authentication methods. +func DeleteMFA(ctx context.Context, params *DeleteMFAParams) (*MultifactorAuthentication, error) { + return getClient().DeleteMFA(ctx, params) +} + +// Ban marks the user as banned. +func Ban(ctx context.Context, id string) (*clerk.User, error) { + return getClient().Ban(ctx, id) +} + +// Unban removes the ban for a user. +func Unban(ctx context.Context, id string) (*clerk.User, error) { + return getClient().Unban(ctx, id) +} + +// Lock marks the user as locked. +func Lock(ctx context.Context, id string) (*clerk.User, error) { + return getClient().Lock(ctx, id) +} + +// Unlock removes the lock for a user. +func Unlock(ctx context.Context, id string) (*clerk.User, error) { + return getClient().Unlock(ctx, id) +} + +// ListOrganizationMemberships lists all the user's organization memberships. +func ListOrganizationMemberships(ctx context.Context, params *ListOrganizationMembershipsParams) (*clerk.OrganizationMembershipList, error) { + return getClient().ListOrganizationMemberships(ctx, params) +} + +func getClient() *Client { + return &Client{ + Backend: clerk.GetBackend(), + } +} diff --git a/user/client.go b/user/client.go new file mode 100644 index 00000000..3877cd4c --- /dev/null +++ b/user/client.go @@ -0,0 +1,373 @@ +// Package user provides the Users API. +package user + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" + + "github.com/clerk/clerk-sdk-go/v2" +) + +//go:generate go run ../cmd/gen/main.go + +const path = "/users" + +// Client is used to invoke the Users API. +type Client struct { + Backend clerk.Backend +} + +type ClientConfig struct { + clerk.BackendConfig +} + +func NewClient(config *ClientConfig) *Client { + return &Client{ + Backend: clerk.NewBackend(&config.BackendConfig), + } +} + +type CreateParams struct { + clerk.APIParams + EmailAddresses []string `json:"email_address,omitempty"` + PhoneNumbers []string `json:"phone_number,omitempty"` + Web3Wallets []string `json:"web3_wallet,omitempty"` + Username *string `json:"username,omitempty"` + Password *string `json:"password,omitempty"` + FirstName *string `json:"first_name,omitempty"` + LastName *string `json:"last_name,omitempty"` + ExternalID *string `json:"external_id,omitempty"` + UnsafeMetadata *json.RawMessage `json:"unsafe_metadata,omitempty"` + PublicMetadata *json.RawMessage `json:"public_metadata,omitempty"` + PrivateMetadata *json.RawMessage `json:"private_metadata,omitempty"` + PasswordDigest *string `json:"password_digest,omitempty"` + PasswordHasher *string `json:"password_hasher,omitempty"` + SkipPasswordRequirement *bool `json:"skip_password_requirement,omitempty"` + SkipPasswordChecks *bool `json:"skip_password_checks,omitempty"` + TOTPSecret *string `json:"totp_secret,omitempty"` + BackupCodes []string `json:"backup_codes,omitempty"` + // Specified in RFC3339 format + CreatedAt *string `json:"created_at,omitempty"` +} + +// Create creates a new user. +func (c *Client) Create(ctx context.Context, params *CreateParams) (*clerk.User, error) { + req := clerk.NewAPIRequest(http.MethodPost, path) + req.SetParams(params) + resource := &clerk.User{} + err := c.Backend.Call(ctx, req, resource) + return resource, err +} + +// Get retrieves details about the user. +func (c *Client) Get(ctx context.Context, id string) (*clerk.User, error) { + path, err := clerk.JoinPath(path, id) + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodGet, path) + resource := &clerk.User{} + err = c.Backend.Call(ctx, req, resource) + return resource, err +} + +type UpdateParams struct { + clerk.APIParams + FirstName *string `json:"first_name,omitempty"` + LastName *string `json:"last_name,omitempty"` + PrimaryEmailAddressID *string `json:"primary_email_address_id,omitempty"` + NotifyPrimaryEmailAddressChanged *bool `json:"notify_primary_email_address_changed,omitempty"` + PrimaryPhoneNumberID *string `json:"primary_phone_number_id,omitempty"` + PrimaryWeb3WalletID *string `json:"primary_web3_wallet_id,omitempty"` + Username *string `json:"username,omitempty"` + ProfileImageID *string `json:"profile_image_id,omitempty"` + ProfileImage *string `json:"profile_image,omitempty"` + Password *string `json:"password,omitempty"` + PasswordDigest *string `json:"password_digest,omitempty"` + PasswordHasher *string `json:"password_hasher,omitempty"` + SkipPasswordChecks *bool `json:"skip_password_checks,omitempty"` + SignOutOfOtherSessions *bool `json:"sign_out_of_other_sessions,omitempty"` + ExternalID *string `json:"external_id,omitempty"` + PublicMetadata *json.RawMessage `json:"public_metadata,omitempty"` + PrivateMetadata *json.RawMessage `json:"private_metadata,omitempty"` + UnsafeMetadata *json.RawMessage `json:"unsafe_metadata,omitempty"` + TOTPSecret *string `json:"totp_secret,omitempty"` + BackupCodes []string `json:"backup_codes,omitempty"` + DeleteSelfEnabled *bool `json:"delete_self_enabled,omitempty"` + CreateOrganizationEnabled *bool `json:"create_organization_enabled,omitempty"` + // Specified in RFC3339 format + CreatedAt *string `json:"created_at,omitempty"` +} + +// Update updates a user. +func (c *Client) Update(ctx context.Context, id string, params *UpdateParams) (*clerk.User, error) { + path, err := clerk.JoinPath(path, id) + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodPatch, path) + req.SetParams(params) + resource := &clerk.User{} + err = c.Backend.Call(ctx, req, resource) + return resource, err +} + +type UpdateMetadataParams struct { + clerk.APIParams + PublicMetadata *json.RawMessage `json:"public_metadata,omitempty"` + PrivateMetadata *json.RawMessage `json:"private_metadata,omitempty"` + UnsafeMetadata *json.RawMessage `json:"unsafe_metadata,omitempty"` +} + +// UpdateMetadata updates the user's metadata by merging the +// provided values with the existing ones. +func (c *Client) UpdateMetadata(ctx context.Context, id string, params *UpdateMetadataParams) (*clerk.User, error) { + path, err := clerk.JoinPath(path, id, "/metadata") + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodPatch, path) + req.SetParams(params) + resource := &clerk.User{} + err = c.Backend.Call(ctx, req, resource) + return resource, err +} + +// Delete deletes a user. +func (c *Client) Delete(ctx context.Context, id string) (*clerk.DeletedResource, error) { + path, err := clerk.JoinPath(path, id) + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodDelete, path) + resource := &clerk.DeletedResource{} + err = c.Backend.Call(ctx, req, resource) + return resource, err +} + +type ListParams struct { + clerk.APIParams + clerk.ListParams + OrderBy *string `json:"order_by,omitempty"` + Query *string `json:"query,omitempty"` + EmailAddresses []string `json:"email_address,omitempty"` + ExternalIDs []string `json:"external_id,omitempty"` + PhoneNumbers []string `json:"phone_number,omitempty"` + Web3Wallets []string `json:"web3_wallet,omitempty"` + Usernames []string `json:"username,omitempty"` + UserIDs []string `json:"user_id,omitempty"` + LastActiveAtSince *int64 `json:"last_active_at_since,omitempty"` +} + +// ToQuery returns url.Values from the params. +func (params *ListParams) ToQuery() url.Values { + q := params.ListParams.ToQuery() + if params.OrderBy != nil { + q.Add("order_by", *params.OrderBy) + } + if params.Query != nil { + q.Add("query", *params.Query) + } + for _, v := range params.EmailAddresses { + q.Add("email_address", v) + } + for _, v := range params.ExternalIDs { + q.Add("external_id", v) + } + for _, v := range params.PhoneNumbers { + q.Add("phone_number", v) + } + for _, v := range params.Web3Wallets { + q.Add("web3_wallet", v) + } + for _, v := range params.Usernames { + q.Add("username", v) + } + for _, v := range params.UserIDs { + q.Add("user_id", v) + } + if params.LastActiveAtSince != nil { + q.Add("limit", strconv.FormatInt(*params.LastActiveAtSince, 10)) + } + return q +} + +// List returns a list of users. +func (c *Client) List(ctx context.Context, params *ListParams) (*clerk.UserList, error) { + // The Clerk API returns the results of GET /v1/users as an + // array. In order to build the final response that includes + // the total count, we need to make two API calls. + // GET /v1/users retrieves the actual results + // GET /v1/users/count retrieves the total count + // The response is then synthesized from the individual responses. + + // GET /v1/users + req := clerk.NewAPIRequest(http.MethodGet, path) + req.SetParams(params) + data := &userList{} + err := c.Backend.Call(ctx, req, data) + if err != nil { + return nil, err + } + + // GET /v1/users/count + totalCount, err := c.Count(ctx, params) + if err != nil { + return nil, err + } + + users := []*clerk.User(*data) + return &clerk.UserList{ + Users: users, + TotalCount: totalCount.TotalCount, + }, nil +} + +// Count returns the total count of users satisfying the parameters. +func (c *Client) Count(ctx context.Context, params *ListParams) (*TotalCount, error) { + path, err := clerk.JoinPath(path, "/count") + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodGet, path) + req.SetParams(params) + resource := &TotalCount{} + err = c.Backend.Call(ctx, req, resource) + return resource, err +} + +// Custom type needed in order to store the GET /v1/users results +// array. +type userList []*clerk.User + +// Read implements the clerk.ResponseReader interface. +// The implementation is empty, meaning that we'll lose +// the raw response from the server. +func (_ *userList) Read(res *clerk.APIResponse) { + // no-op +} + +// Response schema for GET /v1/users/count +type TotalCount struct { + clerk.APIResource + Object string `json:"object"` + TotalCount int64 `json:"total_count"` +} + +type ListOAuthAccessTokensParams struct { + clerk.APIParams + ID string `json:"-"` + Provider string `json:"-"` +} + +// ListOAuthAccessTokens retrieves a list of the user's access +// tokens for a specific OAuth provider. +func (c *Client) ListOAuthAccessTokens(ctx context.Context, params *ListOAuthAccessTokensParams) (*clerk.OAuthAccessTokenList, error) { + path, err := clerk.JoinPath(path, params.ID, "/oauth_access_tokens", fmt.Sprintf("%s?paginated=true", params.Provider)) + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodGet, path) + req.SetParams(params) + list := &clerk.OAuthAccessTokenList{} + err = c.Backend.Call(ctx, req, list) + return list, err +} + +type DeleteMFAParams struct { + clerk.APIParams + ID string `json:"-"` +} + +// DeleteMFA disables a user's multi-factor authentication methods. +func (c *Client) DeleteMFA(ctx context.Context, params *DeleteMFAParams) (*MultifactorAuthentication, error) { + path, err := clerk.JoinPath(path, params.ID, "/mfa") + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodDelete, path) + resource := &MultifactorAuthentication{} + err = c.Backend.Call(ctx, req, resource) + return resource, err +} + +type MultifactorAuthentication struct { + clerk.APIResource + UserID string `json:"user_id"` +} + +// Ban marks the user as banned. +func (c *Client) Ban(ctx context.Context, id string) (*clerk.User, error) { + path, err := clerk.JoinPath(path, id, "/ban") + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodPost, path) + resource := &clerk.User{} + err = c.Backend.Call(ctx, req, resource) + return resource, err +} + +// Unban removes the ban for a user. +func (c *Client) Unban(ctx context.Context, id string) (*clerk.User, error) { + path, err := clerk.JoinPath(path, id, "/unban") + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodPost, path) + resource := &clerk.User{} + err = c.Backend.Call(ctx, req, resource) + return resource, err +} + +// Lock marks the user as locked. +func (c *Client) Lock(ctx context.Context, id string) (*clerk.User, error) { + path, err := clerk.JoinPath(path, id, "/lock") + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodPost, path) + resource := &clerk.User{} + err = c.Backend.Call(ctx, req, resource) + return resource, err +} + +// Unlock removes the lock for a user. +func (c *Client) Unlock(ctx context.Context, id string) (*clerk.User, error) { + path, err := clerk.JoinPath(path, id, "/unlock") + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodPost, path) + resource := &clerk.User{} + err = c.Backend.Call(ctx, req, resource) + return resource, err +} + +type ListOrganizationMembershipsParams struct { + clerk.APIParams + clerk.ListParams + ID string `json:"-"` +} + +// ToQuery returns url.Values from the params. +func (params *ListOrganizationMembershipsParams) ToQuery() url.Values { + return params.ListParams.ToQuery() +} + +// ListOrganizationMemberships lists all the user's organization memberships. +func (c *Client) ListOrganizationMemberships(ctx context.Context, params *ListOrganizationMembershipsParams) (*clerk.OrganizationMembershipList, error) { + path, err := clerk.JoinPath(path, params.ID, "/organization_memberships") + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodGet, path) + req.SetParams(params) + list := &clerk.OrganizationMembershipList{} + err = c.Backend.Call(ctx, req, list) + return list, err +} diff --git a/user/client_test.go b/user/client_test.go new file mode 100644 index 00000000..73e1fbf4 --- /dev/null +++ b/user/client_test.go @@ -0,0 +1,375 @@ +package user + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/clerk/clerk-sdk-go/v2" + "github.com/clerk/clerk-sdk-go/v2/clerktest" + "github.com/stretchr/testify/require" +) + +func TestUserClientCreate(t *testing.T) { + t.Parallel() + id := "user_123" + username := "username" + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + In: json.RawMessage(fmt.Sprintf(`{"username":"%s"}`, username)), + Out: json.RawMessage(fmt.Sprintf(`{"id":"%s","username":"%s"}`, id, username)), + Method: http.MethodPost, + Path: "/v1/users", + }, + } + client := NewClient(config) + user, err := client.Create(context.Background(), &CreateParams{ + Username: clerk.String(username), + }) + require.NoError(t, err) + require.Equal(t, id, user.ID) + require.Equal(t, username, *user.Username) +} + +func TestUserClientList_Request(t *testing.T) { + t.Parallel() + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Method: http.MethodGet, + Query: &url.Values{ + "limit": []string{"1"}, + "offset": []string{"2"}, + "order_by": []string{"-created_at"}, + "email_address": []string{"foo@bar.com", "baz@bar.com"}, + }, + }, + } + client := NewClient(config) + params := &ListParams{ + EmailAddresses: []string{"foo@bar.com", "baz@bar.com"}, + OrderBy: clerk.String("-created_at"), + } + params.Limit = clerk.Int64(1) + params.Offset = clerk.Int64(2) + _, err := client.List(context.Background(), params) + require.NoError(t, err) +} + +func TestUserClientList_Response(t *testing.T) { + t.Parallel() + usersJSON := `[{"object":"user","id":"user_123"}]` + countJSON := `{"object":"total_count","total_count":5}` + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.Contains(r.URL.Path, "count") { + _, err := w.Write([]byte(countJSON)) + require.NoError(t, err) + return + } + _, err := w.Write([]byte(usersJSON)) + require.NoError(t, err) + })) + defer ts.Close() + + config := &ClientConfig{} + config.URL = clerk.String(ts.URL) + config.HTTPClient = ts.Client() + client := NewClient(config) + list, err := client.List(context.Background(), &ListParams{}) + require.NoError(t, err) + require.Equal(t, int64(5), list.TotalCount) + require.Equal(t, 1, len(list.Users)) + require.Equal(t, "user_123", list.Users[0].ID) +} + +func TestUserClientCount(t *testing.T) { + t.Parallel() + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(`{"object":"total_count","total_count":10}`), + Method: http.MethodGet, + Path: "/v1/users/count", + Query: &url.Values{ + "limit": []string{"1"}, + "offset": []string{"2"}, + "order_by": []string{"-created_at"}, + "email_address": []string{"foo@bar.com", "baz@bar.com"}, + }, + }, + } + client := NewClient(config) + params := &ListParams{ + EmailAddresses: []string{"foo@bar.com", "baz@bar.com"}, + OrderBy: clerk.String("-created_at"), + } + params.Limit = clerk.Int64(1) + params.Offset = clerk.Int64(2) + totalCount, err := client.Count(context.Background(), params) + require.NoError(t, err) + require.Equal(t, "total_count", totalCount.Object) + require.Equal(t, int64(10), totalCount.TotalCount) +} + +func TestUserClientGet(t *testing.T) { + t.Parallel() + id := "user_123" + username := "username" + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(fmt.Sprintf(`{"id":"%s","username":"%s"}`, id, username)), + Method: http.MethodGet, + Path: "/v1/users/" + id, + }, + } + client := NewClient(config) + user, err := client.Get(context.Background(), id) + require.NoError(t, err) + require.Equal(t, id, user.ID) + require.Equal(t, username, *user.Username) +} + +func TestUserClientDelete(t *testing.T) { + t.Parallel() + id := "user_123" + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(fmt.Sprintf(`{"id":"%s","deleted":true}`, id)), + Method: http.MethodDelete, + Path: "/v1/users/" + id, + }, + } + client := NewClient(config) + user, err := client.Delete(context.Background(), id) + require.NoError(t, err) + require.Equal(t, id, user.ID) + require.True(t, user.Deleted) +} + +func TestUserClientUpdate(t *testing.T) { + t.Parallel() + id := "user_123" + username := "username" + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + In: json.RawMessage(fmt.Sprintf(`{"username":"%s"}`, username)), + Out: json.RawMessage(fmt.Sprintf(`{"id":"%s","username":"%s"}`, id, username)), + Method: http.MethodPatch, + Path: "/v1/users/" + id, + }, + } + client := NewClient(config) + user, err := client.Update(context.Background(), id, &UpdateParams{ + Username: clerk.String(username), + }) + require.NoError(t, err) + require.Equal(t, id, user.ID) + require.Equal(t, username, *user.Username) +} + +func TestUserClientUpdateMetadata(t *testing.T) { + t.Parallel() + id := "user_123" + metadata := json.RawMessage(`{"foo":"bar"}`) + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + In: json.RawMessage(fmt.Sprintf(`{"private_metadata":%s}`, string(metadata))), + Out: json.RawMessage(fmt.Sprintf(`{"id":"%s","private_metadata":%s}`, id, string(metadata))), + Method: http.MethodPatch, + Path: "/v1/users/" + id + "/metadata", + }, + } + client := NewClient(config) + user, err := client.UpdateMetadata(context.Background(), id, &UpdateMetadataParams{ + PrivateMetadata: &metadata, + }) + require.NoError(t, err) + require.Equal(t, id, user.ID) + require.JSONEq(t, string(metadata), string(user.PrivateMetadata)) +} + +func TestUserClientListOAuthAccessTokens(t *testing.T) { + t.Parallel() + id := "user_123" + provider := "oauth_custom" + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(fmt.Sprintf(`{ +"data":[{ + "provider":"%s", + "token":"the-token" +}], +"total_count":1 +}`, + provider)), + Method: http.MethodGet, + Path: "/v1/users/" + id + "/oauth_access_tokens/" + provider, + Query: &url.Values{ + "paginated": []string{"true"}, + }, + }, + } + client := NewClient(config) + list, err := client.ListOAuthAccessTokens(context.Background(), &ListOAuthAccessTokensParams{ + ID: id, + Provider: provider, + }) + require.NoError(t, err) + require.Equal(t, int64(1), list.TotalCount) + require.Equal(t, 1, len(list.OAuthAccessTokens)) + require.Equal(t, provider, list.OAuthAccessTokens[0].Provider) +} + +func TestUserClientDeleteMFA(t *testing.T) { + t.Parallel() + id := "user_123" + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(fmt.Sprintf(`{"user_id":"%s"}`, id)), + Method: http.MethodDelete, + Path: "/v1/users/" + id + "/mfa", + }, + } + client := NewClient(config) + mfa, err := client.DeleteMFA(context.Background(), &DeleteMFAParams{ + ID: id, + }) + require.NoError(t, err) + require.Equal(t, id, mfa.UserID) +} + +func TestUserClientBan(t *testing.T) { + t.Parallel() + id := "user_123" + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(fmt.Sprintf(`{"object":"user","id":"%s"}`, id)), + Method: http.MethodPost, + Path: "/v1/users/" + id + "/ban", + }, + } + client := NewClient(config) + user, err := client.Ban(context.Background(), id) + require.NoError(t, err) + require.Equal(t, id, user.ID) + require.Equal(t, "user", user.Object) +} + +func TestUserClientUnban(t *testing.T) { + t.Parallel() + id := "user_123" + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(fmt.Sprintf(`{"object":"user","id":"%s"}`, id)), + Method: http.MethodPost, + Path: "/v1/users/" + id + "/unban", + }, + } + client := NewClient(config) + user, err := client.Unban(context.Background(), id) + require.NoError(t, err) + require.Equal(t, id, user.ID) + require.Equal(t, "user", user.Object) +} + +func TestUserClientLock(t *testing.T) { + t.Parallel() + id := "user_123" + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(fmt.Sprintf(`{"object":"user","id":"%s"}`, id)), + Method: http.MethodPost, + Path: "/v1/users/" + id + "/lock", + }, + } + client := NewClient(config) + user, err := client.Lock(context.Background(), id) + require.NoError(t, err) + require.Equal(t, id, user.ID) + require.Equal(t, "user", user.Object) +} + +func TestUserClientUnlock(t *testing.T) { + t.Parallel() + id := "user_123" + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(fmt.Sprintf(`{"object":"user","id":"%s"}`, id)), + Method: http.MethodPost, + Path: "/v1/users/" + id + "/unlock", + }, + } + client := NewClient(config) + user, err := client.Unlock(context.Background(), id) + require.NoError(t, err) + require.Equal(t, id, user.ID) + require.Equal(t, "user", user.Object) +} + +func TestUserClientListOrganizationMemberships(t *testing.T) { + t.Parallel() + membershipID := "orgmem_123" + organizationID := "org_123" + userID := "user_123" + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(fmt.Sprintf(`{ +"data": [{ + "id":"%s", + "organization":{"id":"%s"}, + "public_user_data":{"user_id":"%s"} +}], +"total_count": 1 +}`, + membershipID, organizationID, userID)), + Method: http.MethodGet, + Path: "/v1/users/" + userID + "/organization_memberships", + Query: &url.Values{ + "limit": []string{"1"}, + "offset": []string{"2"}, + }, + }, + } + client := NewClient(config) + params := &ListOrganizationMembershipsParams{ + ID: userID, + } + params.Limit = clerk.Int64(1) + params.Offset = clerk.Int64(2) + list, err := client.ListOrganizationMemberships(context.Background(), params) + require.NoError(t, err) + require.Equal(t, membershipID, list.OrganizationMemberships[0].ID) + require.Equal(t, organizationID, list.OrganizationMemberships[0].Organization.ID) + require.Equal(t, userID, list.OrganizationMemberships[0].PublicUserData.UserID) +}