diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..842e2482 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + push: + branches: + - 'main' + pull_request: {} + +defaults: + run: + shell: bash + +jobs: + test: + name: Test Go ${{ matrix.go }} + runs-on: ubuntu-latest + strategy: + matrix: + go: + - 'stable' + - 'oldstable' + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v4 + with: + go-version: ${{ matrix.go }} + + - name: Run Tests + run: | + go mod download + go test -v ./... + + - name: Code style + run: | + gofmt -d ./ + git diff --exit-code diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml deleted file mode 100644 index 67680344..00000000 --- a/.semaphore/semaphore.yml +++ /dev/null @@ -1,34 +0,0 @@ -version: v1.0 -name: workos-go pipeline -agent: - machine: - type: e1-standard-2 - os_image: ubuntu2004 - -blocks: - - name: Check code style - task: - jobs: - - name: gofmt - commands: - - checkout - - sem-version go 1.18 - - diff -u <(echo -n) <(gofmt -d ./) - - - name: Run tests - task: - prologue: - commands: - - export "SEMAPHORE_GIT_DIR=$(go env GOPATH)/src/github.com/workos/${SEMAPHORE_PROJECT_NAME}" - - export "PATH=$(go env GOPATH)/bin:${PATH}" - - mkdir -vp "${SEMAPHORE_GIT_DIR}" "$(go env GOPATH)/bin" - jobs: - - name: go test - matrix: - - env_var: GO_VERSION - values: ["1.13", "1.18"] - commands: - - checkout - - sem-version go $GO_VERSION - - go mod download - - go test -v ./... diff --git a/README.md b/README.md index f183d386..09eced69 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,19 @@ directorysync.SetAPIKey(""); For our SDKs WorkOS follows a Semantic Versioning ([SemVer](https://semver.org/)) process where all releases will have a version X.Y.Z (like 1.0.0) pattern wherein Z would be a bug fix (e.g., 1.0.1), Y would be a minor release (1.1.0) and X would be a major release (2.0.0). We permit any breaking changes to only be released in major versions and strongly recommend reading changelogs before making any major version upgrades. +## Beta Releases + +WorkOS has features in Beta that can be accessed via Beta releases. We would love for you to try these +and share feedback with us before these features reach general availability (GA). To install a Beta version, +please follow the [installation steps](#installation) above using the Beta release version. + +> Note: there can be breaking changes between Beta versions. Therefore, we recommend pinning the package version to a +> specific version. This way you can install the same version each time without breaking changes unless you are +> intentionally looking for the latest Beta version. + +We highly recommend keeping an eye on when the Beta feature you are interested in goes from Beta to stable so that you +can move to using the stable version. + ## More Information - [User Management Guide](https://workos.com/docs/user-management) diff --git a/internal/workos/workos.go b/internal/workos/workos.go index 4d63af96..7bd792bb 100644 --- a/internal/workos/workos.go +++ b/internal/workos/workos.go @@ -2,5 +2,5 @@ package workos const ( // Version represents the SDK version number. - Version = "v4.0.0" + Version = "v4.11.0" ) diff --git a/pkg/common/role.go b/pkg/common/role.go new file mode 100644 index 00000000..7628c585 --- /dev/null +++ b/pkg/common/role.go @@ -0,0 +1,6 @@ +package common + +type RoleResponse struct { + // The slug of the role + Slug string `json:"slug"` +} diff --git a/pkg/directorysync/client.go b/pkg/directorysync/client.go index 6ce67852..6b10df06 100644 --- a/pkg/directorysync/client.go +++ b/pkg/directorysync/client.go @@ -131,6 +131,9 @@ type User struct { // The User's updated at date UpdatedAt string `json:"updated_at"` + + // The role given to this Directory User + Role common.RoleResponse `json:"role,omitempty"` } // ListUsersOpts contains the options to request provisioned Directory Users. diff --git a/pkg/directorysync/client_test.go b/pkg/directorysync/client_test.go index 6fb333f1..a8749091 100644 --- a/pkg/directorysync/client_test.go +++ b/pkg/directorysync/client_test.go @@ -57,6 +57,9 @@ func TestListUsers(t *testing.T) { State: Active, RawAttributes: json.RawMessage(`{"foo":"bar"}`), CustomAttributes: json.RawMessage(`{"foo":"bar"}`), + Role: common.RoleResponse{ + Slug: "member", + }, }, }, ListMetadata: common.ListMetadata{ @@ -126,6 +129,7 @@ func listUsersTestHandler(w http.ResponseWriter, r *http.Request) { State: Active, RawAttributes: json.RawMessage(`{"foo":"bar"}`), CustomAttributes: json.RawMessage(`{"foo":"bar"}`), + Role: common.RoleResponse{Slug: "member"}, }, }, ListMetadata: common.ListMetadata{ @@ -291,6 +295,7 @@ func TestGetUser(t *testing.T) { State: Active, RawAttributes: json.RawMessage(`{"foo":"bar"}`), CustomAttributes: json.RawMessage(`{"foo":"bar"}`), + Role: common.RoleResponse{Slug: "member"}, }, }, } @@ -349,6 +354,7 @@ func getUserTestHandler(w http.ResponseWriter, r *http.Request) { State: Active, RawAttributes: json.RawMessage(`{"foo":"bar"}`), CustomAttributes: json.RawMessage(`{"foo":"bar"}`), + Role: common.RoleResponse{Slug: "member"}, }) if err != nil { w.WriteHeader(http.StatusInternalServerError) diff --git a/pkg/directorysync/directorysync_test.go b/pkg/directorysync/directorysync_test.go index 2c5bda77..4e278bf3 100644 --- a/pkg/directorysync/directorysync_test.go +++ b/pkg/directorysync/directorysync_test.go @@ -21,6 +21,10 @@ func TestDirectorySyncListUsers(t *testing.T) { } SetAPIKey("test") + expectedRole := common.RoleResponse{ + Slug: "member", + } + expectedResponse := ListUsersResponse{ Data: []User{ User{ @@ -45,6 +49,7 @@ func TestDirectorySyncListUsers(t *testing.T) { State: Active, RawAttributes: json.RawMessage(`{"foo":"bar"}`), CustomAttributes: json.RawMessage(`{"foo":"bar"}`), + Role: expectedRole, }, }, ListMetadata: common.ListMetadata{ @@ -109,6 +114,10 @@ func TestDirectorySyncGetUser(t *testing.T) { } SetAPIKey("test") + expectedRole := common.RoleResponse{ + Slug: "member", + } + expectedResponse := User{ ID: "directory_user_id", FirstName: "Rick", @@ -131,6 +140,7 @@ func TestDirectorySyncGetUser(t *testing.T) { State: Active, RawAttributes: json.RawMessage(`{"foo":"bar"}`), CustomAttributes: json.RawMessage(`{"foo":"bar"}`), + Role: expectedRole, } directoryUserResponse, err := GetUser(context.Background(), GetUserOpts{ User: "directory_user_id", diff --git a/pkg/events/client.go b/pkg/events/client.go index d8c955da..ed8f6254 100644 --- a/pkg/events/client.go +++ b/pkg/events/client.go @@ -40,8 +40,16 @@ const ( UserCreated = "user.created" UserUpdated = "user.updated" UserDeleted = "user.deleted" - OrganizationMembershipAdded = "organization_membership.added" - OrganizationMembershipRemoved = "organization_membership.removed" + OrganizationMembershipAdded = "organization_membership.added" // Deprecated: use OrganizationMembershipCreated instead + OrganizationMembershipCreated = "organization_membership.created" + OrganizationMembershipDeleted = "organization_membership.deleted" + OrganizationMembershipUpdated = "organization_membership.updated" + OrganizationMembershipRemoved = "organization_membership.removed" // Deprecated: use OrganizationMembershipDeleted instead + SessionCreated = "session.created" + EmailVerificationCreated = "email_verification.created" + InvitationCreated = "invitation.created" + MagicAuthCreated = "magic_auth.created" + PasswordResetCreated = "password_reset.created" ) // Client represents a client that performs Event requests to the WorkOS API. @@ -87,7 +95,7 @@ type Event struct { // ListEventsOpts contains the options to request provisioned Events. type ListEventsOpts struct { // Filter to only return Events of particular types. - Events []string `url:"events,omitempty"` + Events []string `url:"events"` // Maximum number of records to return. Limit int `url:"limit"` @@ -100,6 +108,8 @@ type ListEventsOpts struct { // Date range end for stream of Events. RangeEnd string `url:"range_end,omitempty"` + + OrganizationId string `url:"organization_id,omitempty"` } // GetEventsResponse describes the response structure when requesting diff --git a/pkg/events/client_test.go b/pkg/events/client_test.go index b2274595..cbab40fc 100644 --- a/pkg/events/client_test.go +++ b/pkg/events/client_test.go @@ -36,7 +36,10 @@ func TestListEvents(t *testing.T) { }, } - events, err := client.ListEvents(context.Background(), ListEventsOpts{}) + params := ListEventsOpts{ + Events: []string{"dsync.user.created"}, + } + events, err := client.ListEvents(context.Background(), params) require.NoError(t, err) require.Equal(t, expectedResponse, events) @@ -56,6 +59,7 @@ func TestListEvents(t *testing.T) { rangeEnd := currentTime.AddDate(0, 0, -1) params := ListEventsOpts{ + Events: []string{"dsync.user.created"}, RangeStart: rangeStart.String(), RangeEnd: rangeEnd.String(), } @@ -78,6 +82,39 @@ func TestListEvents(t *testing.T) { require.NoError(t, err) require.Equal(t, expectedResponse, events) }) + + t.Run("ListEvents succeeds to fetch Events with an organization_id", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(ListEventsTestHandler)) + defer server.Close() + client := &Client{ + HTTPClient: server.Client(), + Endpoint: server.URL, + APIKey: "test", + } + + params := ListEventsOpts{ + Events: []string{"dsync.user.created"}, + OrganizationId: "org_1234", + } + + expectedResponse := ListEventsResponse{ + Data: []Event{ + { + ID: "event_abcd1234", + Event: "dsync.user.created", + Data: json.RawMessage(`{"foo":"bar"}`), + }, + }, + ListMetadata: common.ListMetadata{ + After: "", + }, + } + + events, err := client.ListEvents(context.Background(), params) + + require.NoError(t, err) + require.Equal(t, expectedResponse, events) + }) } func ListEventsTestHandler(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/events/events_test.go b/pkg/events/events_test.go index 3a045201..4f7760b0 100644 --- a/pkg/events/events_test.go +++ b/pkg/events/events_test.go @@ -21,6 +21,10 @@ func TestEventsListEvents(t *testing.T) { } SetAPIKey("test") + params := ListEventsOpts{ + Events: []string{"dsync.user.created"}, + } + expectedResponse := ListEventsResponse{ Data: []Event{ { @@ -33,7 +37,7 @@ func TestEventsListEvents(t *testing.T) { After: "", }, } - eventsResponse, err := ListEvents(context.Background(), ListEventsOpts{}) + eventsResponse, err := ListEvents(context.Background(), params) require.NoError(t, err) require.Equal(t, expectedResponse, eventsResponse) diff --git a/pkg/organizations/client.go b/pkg/organizations/client.go index 0b104386..73cdf6ff 100644 --- a/pkg/organizations/client.go +++ b/pkg/organizations/client.go @@ -125,6 +125,22 @@ type ListOrganizationsResponse struct { ListMetadata common.ListMetadata `json:"listMetadata"` } +type OrganizationDomainDataState string + +const ( + Verified OrganizationDomainDataState = "verified" + Pending OrganizationDomainDataState = "pending" +) + +// OrganizationDomainData contains data used to create an OrganizationDomain. +type OrganizationDomainData struct { + // The domain's value. + Domain string `json:"domain"` + + // The domain's state. + State OrganizationDomainDataState `json:"state"` +} + // CreateOrganizationOpts contains the options to create an Organization. type CreateOrganizationOpts struct { // Name of the Organization. @@ -135,8 +151,13 @@ type CreateOrganizationOpts struct { AllowProfilesOutsideOrganization bool `json:"allow_profiles_outside_organization"` // Domains of the Organization. + // + // Deprecated: Use DomainData instead. Domains []string `json:"domains"` + // Domains of the Organization. + DomainData []OrganizationDomainData `json:"domain_data"` + // Optional unique identifier to ensure idempotency IdempotencyKey string `json:"idempotency_iey,omitempty"` } @@ -154,7 +175,12 @@ type UpdateOrganizationOpts struct { AllowProfilesOutsideOrganization bool // Domains of the Organization. + // + // Deprecated: Use DomainData instead. Domains []string + + // Domains of the Organization. + DomainData []OrganizationDomainData `json:"domain_data"` } // GetOrganization gets an Organization. diff --git a/pkg/organizations/client_test.go b/pkg/organizations/client_test.go index f47c569b..f2426f35 100644 --- a/pkg/organizations/client_test.go +++ b/pkg/organizations/client_test.go @@ -216,7 +216,7 @@ func TestCreateOrganization(t *testing.T) { err: true, }, { - scenario: "Request returns Organization", + scenario: "Request returns Organization with Domains", client: &Client{ APIKey: "test", }, @@ -236,6 +236,32 @@ func TestCreateOrganization(t *testing.T) { }, }, }, + { + scenario: "Request returns Organization with DomainData", + client: &Client{ + APIKey: "test", + }, + options: CreateOrganizationOpts{ + Name: "Foo Corp", + DomainData: []OrganizationDomainData{ + OrganizationDomainData{ + Domain: "foo-corp.com", + State: "verified", + }, + }, + }, + expected: Organization{ + ID: "organization_id", + Name: "Foo Corp", + AllowProfilesOutsideOrganization: false, + Domains: []OrganizationDomain{ + OrganizationDomain{ + ID: "organization_domain_id", + Domain: "foo-corp.com", + }, + }, + }, + }, { scenario: "Request with duplicate Organization Domain returns error", client: &Client{ @@ -350,7 +376,7 @@ func TestUpdateOrganization(t *testing.T) { err: true, }, { - scenario: "Request returns Organization", + scenario: "Request returns Organization with Domains", client: &Client{ APIKey: "test", }, @@ -375,6 +401,41 @@ func TestUpdateOrganization(t *testing.T) { }, }, }, + { + scenario: "Request returns Organization with DomainData", + client: &Client{ + APIKey: "test", + }, + options: UpdateOrganizationOpts{ + Organization: "organization_id", + Name: "Foo Corp", + DomainData: []OrganizationDomainData{ + OrganizationDomainData{ + Domain: "foo-corp.com", + State: "verified", + }, + OrganizationDomainData{ + Domain: "foo-corp.io", + State: "verified", + }, + }, + }, + expected: Organization{ + ID: "organization_id", + Name: "Foo Corp", + AllowProfilesOutsideOrganization: false, + Domains: []OrganizationDomain{ + OrganizationDomain{ + ID: "organization_domain_id", + Domain: "foo-corp.com", + }, + OrganizationDomain{ + ID: "organization_domain_id_2", + Domain: "foo-corp.io", + }, + }, + }, + }, { scenario: "Request with duplicate Organization Domain returns error", client: &Client{ diff --git a/pkg/usermanagement/client.go b/pkg/usermanagement/client.go index 4ec2337a..13ee87e6 100644 --- a/pkg/usermanagement/client.go +++ b/pkg/usermanagement/client.go @@ -20,6 +20,15 @@ import ( // ResponseLimit is the default number of records to limit a response to. const ResponseLimit = 10 +// ScreenHint represents the screen to redirect the user to in Authkit +type ScreenHint string + +// Constants that enumerate the available screen hints. +const ( + SignUp ScreenHint = "sign-up" + SignIn ScreenHint = "sign-in" +) + // Order represents the order of records. type Order string @@ -29,6 +38,16 @@ const ( Desc Order = "desc" ) +type EmailVerification struct { + ID string `json:"id"` + UserId string `json:"user_id"` + Email string `json:"email"` + ExpiresAt string `json:"expires_at"` + Code string `json:"code"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + // InvitationState represents the state of an Invitation. type InvitationState string @@ -41,16 +60,38 @@ const ( ) type Invitation struct { - ID string `json:"id"` - Email string `json:"email"` - State InvitationState `json:"state"` - AcceptedAt string `json:"accepted_at,omitempty"` - RevokedAt string `json:"revoked_at,omitempty"` - Token string `json:"token"` - OrganizationID string `json:"organization_id,omitempty"` - ExpiresAt string `json:"expires_at"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` + ID string `json:"id"` + Email string `json:"email"` + State InvitationState `json:"state"` + AcceptedAt string `json:"accepted_at,omitempty"` + RevokedAt string `json:"revoked_at,omitempty"` + Token string `json:"token"` + AcceptInvitationUrl string `json:"accept_invitation_url"` + OrganizationID string `json:"organization_id,omitempty"` + InviterUserID string `json:"inviter_user_id,omitempty"` + ExpiresAt string `json:"expires_at"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +type MagicAuth struct { + ID string `json:"id"` + UserId string `json:"user_id"` + Email string `json:"email"` + ExpiresAt string `json:"expires_at"` + Code string `json:"code"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +type PasswordReset struct { + ID string `json:"id"` + UserId string `json:"user_id"` + Email string `json:"email"` + PasswordResetToken string `json:"password_reset_token"` + PasswordResetUrl string `json:"password_reset_url"` + ExpiresAt string `json:"expires_at"` + CreatedAt string `json:"created_at"` } // Organization contains data about a particular Organization. @@ -62,6 +103,16 @@ type Organization struct { Name string `json:"name"` } +// OrganizationMembershipStatus represents the status of an Organization Membership. +type OrganizationMembershipStatus string + +// Constants that enumerate the status of an Organization Membership. +const ( + Active OrganizationMembershipStatus = "active" + Inactive OrganizationMembershipStatus = "inactive" + PendingOrganizationMembership OrganizationMembershipStatus = "pending" +) + // OrganizationMembership contains data about a particular OrganizationMembership. type OrganizationMembership struct { // The Organization Membership's unique identifier. @@ -73,6 +124,12 @@ type OrganizationMembership struct { // The ID of the Organization. OrganizationID string `json:"organization_id"` + // The role given to this Organization Membership + Role common.RoleResponse `json:"role"` + + // The Status of the Organization. + Status OrganizationMembershipStatus `json:"status"` + // CreatedAt is the timestamp of when the OrganizationMembership was created. CreatedAt string `json:"created_at"` @@ -108,6 +165,16 @@ type User struct { ProfilePictureURL string `json:"profile_picture_url"` } +// Represents User identities obtained from external identity providers. +type Identity struct { + // The unique ID of the user in the external identity provider. + IdpID string `json:"idp_id"` + // The type of the identity. + Type string `json:"type"` + // The type of OAuth provider for the identity. + Provider string `json:"provider"` +} + // GetUserOpts contains the options to pass in order to get a user profile. type GetUserOpts struct { // User unique identifier @@ -144,11 +211,13 @@ type ListUsersOpts struct { } type CreateUserOpts struct { - Email string `json:"email"` - Password string `json:"password,omitempty"` - FirstName string `json:"first_name,omitempty"` - LastName string `json:"last_name,omitempty"` - EmailVerified bool `json:"email_verified,omitempty"` + Email string `json:"email"` + Password string `json:"password,omitempty"` + PasswordHash string `json:"password_hash,omitempty"` + PasswordHashType PasswordHashType `json:"password_hash_type,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + EmailVerified bool `json:"email_verified,omitempty"` } // The algorithm originally used to hash the password. @@ -188,6 +257,13 @@ type AuthenticateWithCodeOpts struct { UserAgent string `json:"user_agent,omitempty"` } +type AuthenticateWithRefreshTokenOpts struct { + ClientID string `json:"client_id"` + RefreshToken string `json:"refresh_token"` + IPAddress string `json:"ip_address,omitempty"` + UserAgent string `json:"user_agent,omitempty"` +} + type AuthenticateWithMagicAuthOpts struct { ClientID string `json:"client_id"` Code string `json:"code"` @@ -226,6 +302,14 @@ type AuthenticateWithOrganizationSelectionOpts struct { UserAgent string `json:"user_agent,omitempty"` } +type Impersonator struct { + // The email address of the WorkOS Dashboard user using impersonation. + Email string `json:"email"` + + // The reason provided by the impersonator for impersonating the user. + Reason string `json:"reason"` +} + type AuthenticateResponse struct { User User `json:"user"` @@ -235,6 +319,30 @@ type AuthenticateResponse struct { // If the user is a member of only one organization, this is that organization. // If the user is not a member of any organizations, this is null. OrganizationID string `json:"organization_id"` + + // The AccessToken can be validated to confirm that a user has an active session. + AccessToken string `json:"access_token"` + + // This RefreshToken can be used to obtain a new AccessToken using + // `AuthenticateWithRefreshToken` + RefreshToken string `json:"refresh_token"` + + // Present if the authenticated user is being impersonated. + Impersonator *Impersonator `json:"impersonator"` +} + +type RefreshAuthenticationResponse struct { + // The AccessToken can be validated to confirm that a user has an active session. + AccessToken string `json:"access_token"` + + // This RefreshToken can be used to obtain a new AccessToken using + // `AuthenticateWithRefreshToken` + RefreshToken string `json:"refresh_token"` +} + +type GetEmailVerificationOpts struct { + // The Email Verification's unique identifier. + EmailVerification string } type SendVerificationEmailOpts struct { @@ -249,6 +357,16 @@ type VerifyEmailOpts struct { Code string `json:"code"` } +type GetPasswordResetOpts struct { + // The Password Reset's unique identifier. + PasswordReset string +} + +type CreatePasswordResetOpts struct { + // The email address the password reset is for. + Email string `json:"email"` +} + type SendPasswordResetEmailOpts struct { // The unique ID of the User whose email address will be verified. Email string `json:"email"` @@ -269,6 +387,16 @@ type UserResponse struct { User User `json:"user"` } +type GetMagicAuthOpts struct { + MagicAuth string +} + +type CreateMagicAuthOpts struct { + // The email address the one-time code is for. + Email string `json:"email"` + InvitationToken string `json:"invitation_token,omitempty"` +} + type SendMagicAuthCodeOpts struct { // The email address the one-time code will be sent to. Email string `json:"email"` @@ -309,6 +437,9 @@ type ListOrganizationMembershipsOpts struct { // Filter memberships by User ID. UserID string `url:"user_id,omitempty"` + // Filter memberships by status + Statuses []OrganizationMembershipStatus `url:"statuses,omitempty"` + // Maximum number of records to return. Limit int `url:"limit"` @@ -336,6 +467,16 @@ type CreateOrganizationMembershipOpts struct { // The ID of the Organization in which to add the User as a member. OrganizationID string `json:"organization_id"` + + // The slug of the Role in which to grant this membership. If no RoleSlug is given, the default role will be granted. + // OPTIONAL + RoleSlug string `json:"role_slug,omitempty"` +} + +type UpdateOrganizationMembershipOpts struct { + // The slug of the Role to update to for this membership. + // OPTIONAL + RoleSlug string `json:"role_slug,omitempty"` } type DeleteOrganizationMembershipOpts struct { @@ -343,6 +484,16 @@ type DeleteOrganizationMembershipOpts struct { OrganizationMembership string } +type DeactivateOrganizationMembershipOpts struct { + // Organization Membership unique identifier + OrganizationMembership string +} + +type ReactivateOrganizationMembershipOpts struct { + // Organization Membership unique identifier + OrganizationMembership string +} + type GetInvitationOpts struct { Invitation string } @@ -379,12 +530,25 @@ type SendInvitationOpts struct { OrganizationID string `json:"organization_id,omitempty"` ExpiresInDays int `json:"expires_in_days,omitempty"` InviterUserID string `json:"inviter_user_id,omitempty"` + RoleSlug string `json:"role_slug,omitempty"` } type RevokeInvitationOpts struct { Invitation string } +type RevokeSessionOpts struct { + SessionID string `json:"session_id"` +} + +type ListIdentitiesResult struct { + Identities []Identity `json:"identities"` +} + +type ListIdentitiesOpts struct { + ID string `json:"id"` +} + func NewClient(apiKey string) *Client { return &Client{ APIKey: apiKey, @@ -600,6 +764,48 @@ func (c *Client) DeleteUser(ctx context.Context, opts DeleteUserOpts) error { return workos_errors.TryGetHTTPError(res) } +func (c *Client) ListIdentities(ctx context.Context, opts ListIdentitiesOpts) (ListIdentitiesResult, error) { + endpoint := fmt.Sprintf( + "%s/user_management/users/%s/identities", + c.Endpoint, + opts.ID, + ) + + data, err := c.JSONEncode(opts) + if err != nil { + return ListIdentitiesResult{}, err + } + + req, err := http.NewRequest( + http.MethodGet, + endpoint, + bytes.NewBuffer(data), + ) + if err != nil { + return ListIdentitiesResult{}, err + } + req = req.WithContext(ctx) + req.Header.Set("User-Agent", "workos-go/"+workos.Version) + req.Header.Set("Authorization", "Bearer "+c.APIKey) + req.Header.Set("Content-Type", "application/json") + + res, err := c.HTTPClient.Do(req) + if err != nil { + return ListIdentitiesResult{}, err + } + defer res.Body.Close() + + if err = workos_errors.TryGetHTTPError(res); err != nil { + return ListIdentitiesResult{}, err + } + + var body ListIdentitiesResult + dec := json.NewDecoder(res.Body) + err = dec.Decode(&body) + + return body, err +} + // GetAuthorizationURLOpts contains the options to pass in order to generate // an authorization url. type GetAuthorizationURLOpts struct { @@ -635,6 +841,10 @@ type GetAuthorizationURLOpts struct { // Domain hint that will be passed as a parameter to the IdP login page. // OPTIONAL. DomainHint string + + // ScreenHint represents the screen to redirect the user to when the provider is Authkit. + // OPTIONAL. + ScreenHint ScreenHint } // GetAuthorizationURL generates an OAuth 2.0 authorization URL. @@ -661,10 +871,10 @@ func (c *Client) GetAuthorizationURL(opts GetAuthorizationURLOpts) (*url.URL, er query.Set("provider", string(opts.Provider)) } if opts.ConnectionID != "" { - query.Set("connection", opts.ConnectionID) + query.Set("connection_id", opts.ConnectionID) } if opts.OrganizationID != "" { - query.Set("organization", opts.OrganizationID) + query.Set("organization_id", opts.OrganizationID) } if opts.LoginHint != "" { query.Set("login_hint", opts.LoginHint) @@ -676,6 +886,13 @@ func (c *Client) GetAuthorizationURL(opts GetAuthorizationURLOpts) (*url.URL, er query.Set("state", opts.State) } + if opts.ScreenHint != "" { + if opts.Provider != "authkit" { + return nil, errors.New("provider must be 'authkit' to include a screen hint") + } + query.Set("screen_hint", string(opts.ScreenHint)) + } + u, err := url.ParseRequestURI(c.Endpoint + "/user_management/authorize") if err != nil { return nil, err @@ -787,6 +1004,58 @@ func (c *Client) AuthenticateWithCode(ctx context.Context, opts AuthenticateWith return body, err } +// AuthenticateWithRefreshToken obtains a new AccessToken and RefreshToken for +// an existing session +func (c *Client) AuthenticateWithRefreshToken(ctx context.Context, opts AuthenticateWithRefreshTokenOpts) (RefreshAuthenticationResponse, error) { + payload := struct { + AuthenticateWithRefreshTokenOpts + ClientSecret string `json:"client_secret"` + GrantType string `json:"grant_type"` + }{ + AuthenticateWithRefreshTokenOpts: opts, + ClientSecret: c.APIKey, + GrantType: "refresh_token", + } + + jsonData, err := json.Marshal(payload) + if err != nil { + return RefreshAuthenticationResponse{}, err + } + + req, err := http.NewRequest( + http.MethodPost, + c.Endpoint+"/user_management/authenticate", + bytes.NewBuffer(jsonData), + ) + + if err != nil { + return RefreshAuthenticationResponse{}, err + } + + // Add headers and context to the request + req = req.WithContext(ctx) + req.Header.Set("User-Agent", "workos-go/"+workos.Version) + req.Header.Set("Content-Type", "application/json") + + // Execute the request + res, err := c.HTTPClient.Do(req) + if err != nil { + return RefreshAuthenticationResponse{}, err + } + defer res.Body.Close() + + if err = workos_errors.TryGetHTTPError(res); err != nil { + return RefreshAuthenticationResponse{}, err + } + + // Parse the JSON response + var body RefreshAuthenticationResponse + dec := json.NewDecoder(res.Body) + err = dec.Decode(&body) + + return body, err +} + // AuthenticateWithMagicAuth authenticates a user by verifying a one-time code sent to the user's email address by // the Magic Auth Send Code endpoint. func (c *Client) AuthenticateWithMagicAuth(ctx context.Context, opts AuthenticateWithMagicAuthOpts) (AuthenticateResponse, error) { @@ -992,6 +1261,36 @@ func (c *Client) AuthenticateWithOrganizationSelection(ctx context.Context, opts return body, err } +// GetEmailVerification fetches an EmailVerification object by its ID. +func (c *Client) GetEmailVerification(ctx context.Context, opts GetEmailVerificationOpts) (EmailVerification, error) { + endpoint := fmt.Sprintf("%s/user_management/email_verification/%s", c.Endpoint, opts.EmailVerification) + + req, err := http.NewRequest(http.MethodGet, endpoint, nil) + if err != nil { + return EmailVerification{}, err + } + req = req.WithContext(ctx) + req.Header.Set("User-Agent", "workos-go/"+workos.Version) + req.Header.Set("Authorization", "Bearer "+c.APIKey) + req.Header.Set("Content-Type", "application/json") + + res, err := c.HTTPClient.Do(req) + if err != nil { + return EmailVerification{}, err + } + defer res.Body.Close() + + if err = workos_errors.TryGetHTTPError(res); err != nil { + return EmailVerification{}, err + } + + var body EmailVerification + dec := json.NewDecoder(res.Body) + err = dec.Decode(&body) + + return body, err +} + // SendVerificationEmail creates an email verification challenge and emails verification token to user. func (c *Client) SendVerificationEmail(ctx context.Context, opts SendVerificationEmailOpts) (UserResponse, error) { endpoint := fmt.Sprintf( @@ -1072,8 +1371,76 @@ func (c *Client) VerifyEmail(ctx context.Context, opts VerifyEmailOpts) (UserRes return body, err } -// SendPasswordResetEmail creates a password reset challenge and emails a password reset link to an -// unmanaged user. +// GetPasswordReset fetches a PasswordReset object by its ID. +func (c *Client) GetPasswordReset(ctx context.Context, opts GetPasswordResetOpts) (PasswordReset, error) { + endpoint := fmt.Sprintf("%s/user_management/password_reset/%s", c.Endpoint, opts.PasswordReset) + + req, err := http.NewRequest(http.MethodGet, endpoint, nil) + if err != nil { + return PasswordReset{}, err + } + req = req.WithContext(ctx) + req.Header.Set("User-Agent", "workos-go/"+workos.Version) + req.Header.Set("Authorization", "Bearer "+c.APIKey) + req.Header.Set("Content-Type", "application/json") + + res, err := c.HTTPClient.Do(req) + if err != nil { + return PasswordReset{}, err + } + defer res.Body.Close() + + if err = workos_errors.TryGetHTTPError(res); err != nil { + return PasswordReset{}, err + } + + var body PasswordReset + dec := json.NewDecoder(res.Body) + err = dec.Decode(&body) + + return body, err +} + +// CreatePasswordReset creates a PasswordReset token that can be emailed to the user. +func (c *Client) CreatePasswordReset(ctx context.Context, opts CreatePasswordResetOpts) (PasswordReset, error) { + endpoint := fmt.Sprintf("%s/user_management/password_reset", c.Endpoint) + + data, err := json.Marshal(opts) + if err != nil { + return PasswordReset{}, err + } + + req, err := http.NewRequest( + http.MethodPost, + endpoint, + bytes.NewBuffer(data), + ) + if err != nil { + return PasswordReset{}, err + } + req = req.WithContext(ctx) + req.Header.Set("User-Agent", "workos-go/"+workos.Version) + req.Header.Set("Authorization", "Bearer "+c.APIKey) + req.Header.Set("Content-Type", "application/json") + + res, err := c.HTTPClient.Do(req) + if err != nil { + return PasswordReset{}, err + } + defer res.Body.Close() + + if err = workos_errors.TryGetHTTPError(res); err != nil { + return PasswordReset{}, err + } + + var body PasswordReset + dec := json.NewDecoder(res.Body) + err = dec.Decode(&body) + + return body, err +} + +// Deprecated: Use CreatePasswordReset instead. This method will be removed in a future major version. func (c *Client) SendPasswordResetEmail(ctx context.Context, opts SendPasswordResetEmailOpts) error { endpoint := fmt.Sprintf( "%s/user_management/password_reset/send", @@ -1149,7 +1516,76 @@ func (c *Client) ResetPassword(ctx context.Context, opts ResetPasswordOpts) (Use return body, err } -// SendMagicAuthCode creates a one-time Magic Auth code and emails it to the user. +// GetMagicAuth fetches a Magic Auth object by its ID. +func (c *Client) GetMagicAuth(ctx context.Context, opts GetMagicAuthOpts) (MagicAuth, error) { + endpoint := fmt.Sprintf("%s/user_management/magic_auth/%s", c.Endpoint, opts.MagicAuth) + + req, err := http.NewRequest(http.MethodGet, endpoint, nil) + if err != nil { + return MagicAuth{}, err + } + req = req.WithContext(ctx) + req.Header.Set("User-Agent", "workos-go/"+workos.Version) + req.Header.Set("Authorization", "Bearer "+c.APIKey) + req.Header.Set("Content-Type", "application/json") + + res, err := c.HTTPClient.Do(req) + if err != nil { + return MagicAuth{}, err + } + defer res.Body.Close() + + if err = workos_errors.TryGetHTTPError(res); err != nil { + return MagicAuth{}, err + } + + var body MagicAuth + dec := json.NewDecoder(res.Body) + err = dec.Decode(&body) + + return body, err +} + +// CreateMagicAuth creates a one-time Magic Auth code that can be emailed to the user. +func (c *Client) CreateMagicAuth(ctx context.Context, opts CreateMagicAuthOpts) (MagicAuth, error) { + endpoint := fmt.Sprintf("%s/user_management/magic_auth", c.Endpoint) + + data, err := json.Marshal(opts) + if err != nil { + return MagicAuth{}, err + } + + req, err := http.NewRequest( + http.MethodPost, + endpoint, + bytes.NewBuffer(data), + ) + if err != nil { + return MagicAuth{}, err + } + req = req.WithContext(ctx) + req.Header.Set("User-Agent", "workos-go/"+workos.Version) + req.Header.Set("Authorization", "Bearer "+c.APIKey) + req.Header.Set("Content-Type", "application/json") + + res, err := c.HTTPClient.Do(req) + if err != nil { + return MagicAuth{}, err + } + defer res.Body.Close() + + if err = workos_errors.TryGetHTTPError(res); err != nil { + return MagicAuth{}, err + } + + var body MagicAuth + dec := json.NewDecoder(res.Body) + err = dec.Decode(&body) + + return body, err +} + +// Deprecated: Use CreateMagicAuth instead. This method will be removed in a future major version. func (c *Client) SendMagicAuthCode(ctx context.Context, opts SendMagicAuthCodeOpts) error { endpoint := fmt.Sprintf( "%s/user_management/magic_auth/send", @@ -1426,6 +1862,129 @@ func (c *Client) DeleteOrganizationMembership(ctx context.Context, opts DeleteOr return workos_errors.TryGetHTTPError(res) } +// Update an Organization Membership +func (c *Client) UpdateOrganizationMembership( + ctx context.Context, + organizationMembershipId string, + opts UpdateOrganizationMembershipOpts, +) (OrganizationMembership, error) { + endpoint := fmt.Sprintf( + "%s/user_management/organization_memberships/%s", + c.Endpoint, + organizationMembershipId, + ) + + data, err := c.JSONEncode(opts) + if err != nil { + return OrganizationMembership{}, err + } + + req, err := http.NewRequest( + http.MethodPut, + endpoint, + bytes.NewBuffer(data), + ) + if err != nil { + return OrganizationMembership{}, err + } + req = req.WithContext(ctx) + req.Header.Set("User-Agent", "workos-go/"+workos.Version) + req.Header.Set("Authorization", "Bearer "+c.APIKey) + req.Header.Set("Content-Type", "application/json") + + res, err := c.HTTPClient.Do(req) + if err != nil { + return OrganizationMembership{}, err + } + defer res.Body.Close() + + if err = workos_errors.TryGetHTTPError(res); err != nil { + return OrganizationMembership{}, err + } + + var body OrganizationMembership + dec := json.NewDecoder(res.Body) + err = dec.Decode(&body) + + return body, err +} + +// DeactivateOrganizationMembership deactivates an Organization Membership +func (c *Client) DeactivateOrganizationMembership(ctx context.Context, opts DeactivateOrganizationMembershipOpts) (OrganizationMembership, error) { + endpoint := fmt.Sprintf( + "%s/user_management/organization_memberships/%s/deactivate", + c.Endpoint, + opts.OrganizationMembership, + ) + + req, err := http.NewRequest( + http.MethodPut, + endpoint, + nil, + ) + if err != nil { + return OrganizationMembership{}, err + } + req = req.WithContext(ctx) + req.Header.Set("User-Agent", "workos-go/"+workos.Version) + req.Header.Set("Authorization", "Bearer "+c.APIKey) + req.Header.Set("Content-Type", "application/json") + + res, err := c.HTTPClient.Do(req) + if err != nil { + return OrganizationMembership{}, err + } + defer res.Body.Close() + + if err = workos_errors.TryGetHTTPError(res); err != nil { + return OrganizationMembership{}, err + } + + var body OrganizationMembership + dec := json.NewDecoder(res.Body) + err = dec.Decode(&body) + + return body, err +} + +// ReactivateOrganizationMembership reactivates an Organization Membership +func (c *Client) ReactivateOrganizationMembership(ctx context.Context, opts ReactivateOrganizationMembershipOpts) (OrganizationMembership, error) { + endpoint := fmt.Sprintf( + "%s/user_management/organization_memberships/%s/reactivate", + c.Endpoint, + opts.OrganizationMembership, + ) + + req, err := http.NewRequest( + http.MethodPut, + endpoint, + nil, + ) + if err != nil { + return OrganizationMembership{}, err + } + req = req.WithContext(ctx) + req.Header.Set("User-Agent", "workos-go/"+workos.Version) + req.Header.Set("Authorization", "Bearer "+c.APIKey) + req.Header.Set("Content-Type", "application/json") + + res, err := c.HTTPClient.Do(req) + if err != nil { + return OrganizationMembership{}, err + } + defer res.Body.Close() + + if err = workos_errors.TryGetHTTPError(res); err != nil { + return OrganizationMembership{}, err + } + + var body OrganizationMembership + dec := json.NewDecoder(res.Body) + err = dec.Decode(&body) + + return body, err +} + // GetInvitation fetches an Invitation by its ID. func (c *Client) GetInvitation(ctx context.Context, opts GetInvitationOpts) (Invitation, error) { endpoint := fmt.Sprintf("%s/user_management/invitations/%s", c.Endpoint, opts.Invitation) @@ -1574,3 +2133,73 @@ func (c *Client) RevokeInvitation(ctx context.Context, opts RevokeInvitationOpts return body, err } + +func (c *Client) GetJWKSURL(clientID string) (*url.URL, error) { + if clientID == "" { + return nil, errors.New("clientID must not be blank") + } + + u, err := url.ParseRequestURI(c.Endpoint + "/sso/jwks/" + clientID) + if err != nil { + return nil, err + } + + return u, nil +} + +type GetLogoutURLOpts struct { + // The ID of the session that will end. This is in the `sid` claim of the + // AccessToken + // + // REQUIRED + SessionID string +} + +func (c *Client) GetLogoutURL(opts GetLogoutURLOpts) (*url.URL, error) { + if opts.SessionID == "" { + return nil, errors.New("incomplete arguments: missing SessionID") + } + + u, err := url.ParseRequestURI(c.Endpoint + "/user_management/sessions/logout") + if err != nil { + return nil, err + } + + query := make(url.Values, 1) + query.Set("session_id", opts.SessionID) + u.RawQuery = query.Encode() + + return u, nil +} + +func (c *Client) RevokeSession(ctx context.Context, opts RevokeSessionOpts) error { + jsonData, err := json.Marshal(opts) + if err != nil { + return err + } + + req, err := http.NewRequest( + http.MethodPost, + fmt.Sprintf("%s/user_management/sessions/revoke", c.Endpoint), + bytes.NewBuffer(jsonData), + ) + if err != nil { + return err + } + req = req.WithContext(ctx) + req.Header.Set("User-Agent", "workos-go/"+workos.Version) + req.Header.Set("Authorization", "Bearer "+c.APIKey) + req.Header.Set("Content-Type", "application/json") + + res, err := c.HTTPClient.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + + if err = workos_errors.TryGetHTTPError(res); err != nil { + return err + } + + return nil +} diff --git a/pkg/usermanagement/client_test.go b/pkg/usermanagement/client_test.go index a2ec0b71..1f386241 100644 --- a/pkg/usermanagement/client_test.go +++ b/pkg/usermanagement/client_test.go @@ -509,6 +509,92 @@ func deleteUserTestHandler(w http.ResponseWriter, r *http.Request) { w.Write(body) } +func TestListIdentities(t *testing.T) { + tests := []struct { + scenario string + client *Client + options ListIdentitiesOpts + expected ListIdentitiesResult + err bool + }{ + { + scenario: "Request without API Key returns an error", + client: NewClient(""), + err: true, + }, + { + scenario: "Request returns identities", + client: NewClient("test"), + options: ListIdentitiesOpts{ + ID: "user_01E3JC5F5Z1YJNPGVYWV9SX6GH", + }, + expected: ListIdentitiesResult{ + Identities: []Identity{ + { + IdpID: "13966412", + Type: "OAuth", + Provider: "GitHubOAuth", + }, + }, + }, + err: false, + }, + } + + for _, test := range tests { + t.Run(test.scenario, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(listIdentitiesTestHandler)) + defer server.Close() + + client := test.client + client.Endpoint = server.URL + client.HTTPClient = server.Client() + + identities, err := client.ListIdentities(context.Background(), test.options) + if test.err { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, test.expected, identities) + }) + } +} + +func listIdentitiesTestHandler(w http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + if auth != "Bearer test" { + http.Error(w, "bad auth", http.StatusUnauthorized) + return + } + + if r.Method != http.MethodGet { + http.Error(w, "bad method", http.StatusBadRequest) + } + + if userAgent := r.Header.Get("User-Agent"); !strings.Contains(userAgent, "workos-go/") { + w.WriteHeader(http.StatusBadRequest) + return + } + + body, err := json.Marshal(ListIdentitiesResult{ + Identities: []Identity{ + { + IdpID: "13966412", + Type: "OAuth", + Provider: "GitHubOAuth", + }, + }, + }) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Write(body) +} + func TestClientAuthorizeURL(t *testing.T) { tests := []struct { scenario string @@ -525,6 +611,16 @@ func TestClientAuthorizeURL(t *testing.T) { }, expected: "https://api.workos.com/user_management/authorize?client_id=client_123&provider=GoogleOAuth&redirect_uri=https%3A%2F%2Fexample.com%2Fsso%2Fworkos%2Fcallback&response_type=code&state=custom+state", }, + { + scenario: "generate url with a screen hint", + options: GetAuthorizationURLOpts{ + ClientID: "client_123", + Provider: "authkit", + RedirectURI: "https://example.com/sso/workos/callback", + ScreenHint: "sign-up", + }, + expected: "https://api.workos.com/user_management/authorize?client_id=client_123&provider=authkit&redirect_uri=https%3A%2F%2Fexample.com%2Fsso%2Fworkos%2Fcallback&response_type=code&screen_hint=sign-up", + }, { scenario: "generate url with connection", options: GetAuthorizationURLOpts{ @@ -533,7 +629,7 @@ func TestClientAuthorizeURL(t *testing.T) { RedirectURI: "https://example.com/sso/workos/callback", State: "custom state", }, - expected: "https://api.workos.com/user_management/authorize?client_id=client_123&connection=connection_123&redirect_uri=https%3A%2F%2Fexample.com%2Fsso%2Fworkos%2Fcallback&response_type=code&state=custom+state", + expected: "https://api.workos.com/user_management/authorize?client_id=client_123&connection_id=connection_123&redirect_uri=https%3A%2F%2Fexample.com%2Fsso%2Fworkos%2Fcallback&response_type=code&state=custom+state", }, { scenario: "generate url with state", @@ -554,7 +650,7 @@ func TestClientAuthorizeURL(t *testing.T) { RedirectURI: "https://example.com/sso/workos/callback", State: "custom state", }, - expected: "https://api.workos.com/user_management/authorize?client_id=client_123&connection=connection_123&provider=GoogleOAuth&redirect_uri=https%3A%2F%2Fexample.com%2Fsso%2Fworkos%2Fcallback&response_type=code&state=custom+state", + expected: "https://api.workos.com/user_management/authorize?client_id=client_123&connection_id=connection_123&provider=GoogleOAuth&redirect_uri=https%3A%2F%2Fexample.com%2Fsso%2Fworkos%2Fcallback&response_type=code&state=custom+state", }, { scenario: "generate url with organization", @@ -564,7 +660,7 @@ func TestClientAuthorizeURL(t *testing.T) { RedirectURI: "https://example.com/sso/workos/callback", State: "custom state", }, - expected: "https://api.workos.com/user_management/authorize?client_id=client_123&organization=organization_123&redirect_uri=https%3A%2F%2Fexample.com%2Fsso%2Fworkos%2Fcallback&response_type=code&state=custom+state", + expected: "https://api.workos.com/user_management/authorize?client_id=client_123&organization_id=organization_123&redirect_uri=https%3A%2F%2Fexample.com%2Fsso%2Fworkos%2Fcallback&response_type=code&state=custom+state", }, { scenario: "generate url with DomainHint", @@ -575,7 +671,7 @@ func TestClientAuthorizeURL(t *testing.T) { State: "custom state", DomainHint: "foo.com", }, - expected: "https://api.workos.com/user_management/authorize?client_id=client_123&connection=connection_123&domain_hint=foo.com&redirect_uri=https%3A%2F%2Fexample.com%2Fsso%2Fworkos%2Fcallback&response_type=code&state=custom+state", + expected: "https://api.workos.com/user_management/authorize?client_id=client_123&connection_id=connection_123&domain_hint=foo.com&redirect_uri=https%3A%2F%2Fexample.com%2Fsso%2Fworkos%2Fcallback&response_type=code&state=custom+state", }, { scenario: "generate url with LoginHint", @@ -586,7 +682,7 @@ func TestClientAuthorizeURL(t *testing.T) { State: "custom state", LoginHint: "foo@workos.com", }, - expected: "https://api.workos.com/user_management/authorize?client_id=client_123&connection=connection_123&login_hint=foo%40workos.com&redirect_uri=https%3A%2F%2Fexample.com%2Fsso%2Fworkos%2Fcallback&response_type=code&state=custom+state", + expected: "https://api.workos.com/user_management/authorize?client_id=client_123&connection_id=connection_123&login_hint=foo%40workos.com&redirect_uri=https%3A%2F%2Fexample.com%2Fsso%2Fworkos%2Fcallback&response_type=code&state=custom+state", }, } @@ -666,6 +762,8 @@ func TestAuthenticateUserWithPassword(t *testing.T) { Email: "employee@foo-corp.com", }, OrganizationID: "org_123", + AccessToken: "access_token", + RefreshToken: "refresh_token", }, }, } @@ -716,6 +814,31 @@ func TestAuthenticateUserWithCode(t *testing.T) { Email: "employee@foo-corp.com", }, OrganizationID: "org_123", + AccessToken: "access_token", + RefreshToken: "refresh_token", + }, + }, + { + scenario: "Request returns a User and Impersonator metadata", + client: NewClient("test_with_impersonation"), + options: AuthenticateWithCodeOpts{ + ClientID: "project_123", + Code: "test_123", + }, + expected: AuthenticateResponse{ + User: User{ + ID: "testUserID", + FirstName: "John", + LastName: "Doe", + Email: "employee@foo-corp.com", + }, + OrganizationID: "org_123", + AccessToken: "access_token", + RefreshToken: "refresh_token", + Impersonator: &Impersonator{ + Email: "admin@example.com", + Reason: "Helping debug a customer issue.", + }, }, }, } @@ -739,6 +862,51 @@ func TestAuthenticateUserWithCode(t *testing.T) { } } +func TestAuthenticateUserWithRefreshToken(t *testing.T) { + tests := []struct { + scenario string + client *Client + options AuthenticateWithRefreshTokenOpts + expected RefreshAuthenticationResponse + err bool + }{{ + scenario: "Request without API Key returns an error", + client: NewClient(""), + err: true, + }, + { + scenario: "Request new tokens", + client: NewClient("test"), + options: AuthenticateWithRefreshTokenOpts{ + ClientID: "project_123", + RefreshToken: "refresh_token", + }, + expected: RefreshAuthenticationResponse{ + AccessToken: "access_token", + RefreshToken: "new_refresh_token", + }, + }, + } + for _, test := range tests { + t.Run(test.scenario, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(refreshAuthenticationResponseTestHandler)) + defer server.Close() + + client := test.client + client.Endpoint = server.URL + client.HTTPClient = server.Client() + + response, err := client.AuthenticateWithRefreshToken(context.Background(), test.options) + if test.err { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, test.expected, response) + }) + } +} + func TestAuthenticateUserWithMagicAuth(t *testing.T) { tests := []struct { scenario string @@ -768,6 +936,8 @@ func TestAuthenticateUserWithMagicAuth(t *testing.T) { Email: "employee@foo-corp.com", }, OrganizationID: "org_123", + AccessToken: "access_token", + RefreshToken: "refresh_token", }, }, } @@ -820,6 +990,8 @@ func TestAuthenticateUserWithTOTP(t *testing.T) { Email: "employee@foo-corp.com", }, OrganizationID: "org_123", + AccessToken: "access_token", + RefreshToken: "refresh_token", }, }, } @@ -871,6 +1043,8 @@ func TestAuthenticateUserWithEmailVerificationCode(t *testing.T) { Email: "employee@foo-corp.com", }, OrganizationID: "org_123", + AccessToken: "access_token", + RefreshToken: "refresh_token", }, }, } @@ -922,6 +1096,8 @@ func TestAuthenticateUserWithOrganizationSelection(t *testing.T) { Email: "employee@foo-corp.com", }, OrganizationID: "org_123", + AccessToken: "access_token", + RefreshToken: "refresh_token", }, }, } @@ -946,6 +1122,55 @@ func TestAuthenticateUserWithOrganizationSelection(t *testing.T) { } func authenticationResponseTestHandler(w http.ResponseWriter, r *http.Request) { + payload := make(map[string]interface{}) + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + if secret, exists := payload["client_secret"].(string); exists && secret != "" { + var response AuthenticateResponse + + switch secret { + case "test": + response = AuthenticateResponse{ + User: User{ + ID: "testUserID", + FirstName: "John", + LastName: "Doe", + Email: "employee@foo-corp.com", + }, + AccessToken: "access_token", + RefreshToken: "refresh_token", + OrganizationID: "org_123", + } + case "test_with_impersonation": + response = AuthenticateResponse{ + User: User{ + ID: "testUserID", + FirstName: "John", + LastName: "Doe", + Email: "employee@foo-corp.com", + }, + OrganizationID: "org_123", + AccessToken: "access_token", + RefreshToken: "refresh_token", + Impersonator: &Impersonator{ + Email: "admin@example.com", + Reason: "Helping debug a customer issue.", + }, + } + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) + return + } + + w.WriteHeader(http.StatusUnauthorized) +} + +func refreshAuthenticationResponseTestHandler(w http.ResponseWriter, r *http.Request) { payload := make(map[string]interface{}) if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { @@ -954,14 +1179,10 @@ func authenticationResponseTestHandler(w http.ResponseWriter, r *http.Request) { } if secret, exists := payload["client_secret"].(string); exists && secret != "" { response := AuthenticateResponse{ - User: User{ - ID: "testUserID", - FirstName: "John", - LastName: "Doe", - Email: "employee@foo-corp.com", - }, - OrganizationID: "org_123", + AccessToken: "access_token", + RefreshToken: "new_refresh_token", } + w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(response) return @@ -970,6 +1191,87 @@ func authenticationResponseTestHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnauthorized) } +func TestGetEmailVerification(t *testing.T) { + tests := []struct { + scenario string + client *Client + options GetEmailVerificationOpts + expected EmailVerification + err bool + }{ + { + scenario: "Request without API Key returns an error", + client: NewClient(""), + err: true, + }, + { + scenario: "Request returns EmailVerification by ID", + client: NewClient("test"), + options: GetEmailVerificationOpts{EmailVerification: "email_verification_123"}, + expected: EmailVerification{ + ID: "email_verification_123", + UserId: "user_123", + Email: "marcelina@foo-corp.com", + ExpiresAt: "2021-06-25T19:07:33.155Z", + Code: "123456", + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", + }, + }, + } + + for _, test := range tests { + t.Run(test.scenario, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(getEmailVerificationTestHandler)) + defer server.Close() + + client := test.client + client.Endpoint = server.URL + client.HTTPClient = server.Client() + + emailVerification, err := client.GetEmailVerification(context.Background(), test.options) + if test.err { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, test.expected, emailVerification) + }) + } +} + +func getEmailVerificationTestHandler(w http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + if auth != "Bearer test" { + http.Error(w, "bad auth", http.StatusUnauthorized) + return + } + + var body []byte + var err error + + if r.URL.Path == "/user_management/email_verification/email_verification_123" { + emailVerification := EmailVerification{ + ID: "email_verification_123", + UserId: "user_123", + Email: "marcelina@foo-corp.com", + ExpiresAt: "2021-06-25T19:07:33.155Z", + Code: "123456", + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", + } + body, err = json.Marshal(emailVerification) + } + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Write(body) +} + func TestSendVerificationEmail(t *testing.T) { tests := []struct { scenario string @@ -1140,11 +1442,12 @@ func verifyEmailCodeTestHandler(w http.ResponseWriter, r *http.Request) { w.Write(body) } -func TestSendPasswordResetEmail(t *testing.T) { +func TestGetPasswordReset(t *testing.T) { tests := []struct { scenario string client *Client - options SendPasswordResetEmailOpts + options GetPasswordResetOpts + expected PasswordReset err bool }{ { @@ -1153,50 +1456,79 @@ func TestSendPasswordResetEmail(t *testing.T) { err: true, }, { - scenario: "Successful request", + scenario: "Request returns PasswordReset by ID", client: NewClient("test"), - options: SendPasswordResetEmailOpts{ - Email: "marcelina@foo-corp.com", - PasswordResetUrl: "https://foo-corp.com/reset-password", + options: GetPasswordResetOpts{PasswordReset: "password_reset_123"}, + expected: PasswordReset{ + ID: "password_reset_123", + UserId: "user_123", + Email: "marcelina@foo-corp.com", + PasswordResetToken: "myToken", + PasswordResetUrl: "https://your-app.com/reset-password?token=myToken", + ExpiresAt: "2021-06-25T19:07:33.155Z", + CreatedAt: "2021-06-25T19:07:33.155Z", }, }, } for _, test := range tests { t.Run(test.scenario, func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(sendPasswordResetEmailTestHandler)) + server := httptest.NewServer(http.HandlerFunc(getPasswordResetTestHandler)) defer server.Close() client := test.client client.Endpoint = server.URL client.HTTPClient = server.Client() - err := client.SendPasswordResetEmail(context.Background(), test.options) + passwordReset, err := client.GetPasswordReset(context.Background(), test.options) if test.err { require.Error(t, err) return } require.NoError(t, err) + require.Equal(t, test.expected, passwordReset) }) } } -func sendPasswordResetEmailTestHandler(w http.ResponseWriter, r *http.Request) { +func getPasswordResetTestHandler(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization") if auth != "Bearer test" { http.Error(w, "bad auth", http.StatusUnauthorized) return } + var body []byte + var err error + + if r.URL.Path == "/user_management/password_reset/password_reset_123" { + passwordReset := PasswordReset{ + ID: "password_reset_123", + UserId: "user_123", + Email: "marcelina@foo-corp.com", + PasswordResetToken: "myToken", + PasswordResetUrl: "https://your-app.com/reset-password?token=myToken", + ExpiresAt: "2021-06-25T19:07:33.155Z", + CreatedAt: "2021-06-25T19:07:33.155Z", + } + body, err = json.Marshal(passwordReset) + } + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + w.Write(body) } -func TestResetPassword(t *testing.T) { +func TestCreatePasswordReset(t *testing.T) { tests := []struct { scenario string client *Client - options ResetPasswordOpts - expected UserResponse + options CreatePasswordResetOpts + expected PasswordReset err bool }{ { @@ -1205,34 +1537,168 @@ func TestResetPassword(t *testing.T) { err: true, }, { - scenario: "Request returns User", + scenario: "Request returns Password Reset", client: NewClient("test"), - options: ResetPasswordOpts{ - Token: "testToken", + options: CreatePasswordResetOpts{ + Email: "marcelina@foo-corp.com", }, - expected: UserResponse{ - User: User{ - ID: "user_123", - - Email: "marcelina@foo-corp.com", - FirstName: "Marcelina", - LastName: "Davis", - EmailVerified: true, - }, + expected: PasswordReset{ + ID: "password_reset_123", + UserId: "user_123", + Email: "marcelina@foo-corp.com", + PasswordResetToken: "myToken", + PasswordResetUrl: "https://your-app.com/reset-password?token=myToken", + ExpiresAt: "2021-06-25T19:07:33.155Z", + CreatedAt: "2021-06-25T19:07:33.155Z", }, }, } for _, test := range tests { t.Run(test.scenario, func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(resetPasswordHandler)) + server := httptest.NewServer(http.HandlerFunc(CreatePasswordResetTestHandler)) defer server.Close() client := test.client client.Endpoint = server.URL client.HTTPClient = server.Client() - user, err := client.ResetPassword(context.Background(), test.options) + passwordReset, err := client.CreatePasswordReset(context.Background(), test.options) + if test.err { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, test.expected, passwordReset) + }) + } +} + +func CreatePasswordResetTestHandler(w http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + if auth != "Bearer test" { + http.Error(w, "bad auth", http.StatusUnauthorized) + return + } + + var body []byte + var err error + + if r.URL.Path == "/user_management/password_reset" { + body, err = json.Marshal( + PasswordReset{ + ID: "password_reset_123", + UserId: "user_123", + Email: "marcelina@foo-corp.com", + PasswordResetToken: "myToken", + PasswordResetUrl: "https://your-app.com/reset-password?token=myToken", + ExpiresAt: "2021-06-25T19:07:33.155Z", + CreatedAt: "2021-06-25T19:07:33.155Z", + }) + } + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Write(body) +} + +func TestSendPasswordResetEmail(t *testing.T) { + tests := []struct { + scenario string + client *Client + options SendPasswordResetEmailOpts + err bool + }{ + { + scenario: "Request without API Key returns an error", + client: NewClient(""), + err: true, + }, + { + scenario: "Successful request", + client: NewClient("test"), + options: SendPasswordResetEmailOpts{ + Email: "marcelina@foo-corp.com", + PasswordResetUrl: "https://foo-corp.com/reset-password", + }, + }, + } + + for _, test := range tests { + t.Run(test.scenario, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(sendPasswordResetEmailTestHandler)) + defer server.Close() + + client := test.client + client.Endpoint = server.URL + client.HTTPClient = server.Client() + + err := client.SendPasswordResetEmail(context.Background(), test.options) + if test.err { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func sendPasswordResetEmailTestHandler(w http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + if auth != "Bearer test" { + http.Error(w, "bad auth", http.StatusUnauthorized) + return + } + + w.WriteHeader(http.StatusOK) +} + +func TestResetPassword(t *testing.T) { + tests := []struct { + scenario string + client *Client + options ResetPasswordOpts + expected UserResponse + err bool + }{ + { + scenario: "Request without API Key returns an error", + client: NewClient(""), + err: true, + }, + { + scenario: "Request returns User", + client: NewClient("test"), + options: ResetPasswordOpts{ + Token: "testToken", + }, + expected: UserResponse{ + User: User{ + ID: "user_123", + + Email: "marcelina@foo-corp.com", + FirstName: "Marcelina", + LastName: "Davis", + EmailVerified: true, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.scenario, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(resetPasswordHandler)) + defer server.Close() + + client := test.client + client.Endpoint = server.URL + client.HTTPClient = server.Client() + + user, err := client.ResetPassword(context.Background(), test.options) if test.err { require.Error(t, err) return @@ -1275,6 +1741,170 @@ func resetPasswordHandler(w http.ResponseWriter, r *http.Request) { w.Write(body) } +func TestGetMagicAuth(t *testing.T) { + tests := []struct { + scenario string + client *Client + options GetMagicAuthOpts + expected MagicAuth + err bool + }{ + { + scenario: "Request without API Key returns an error", + client: NewClient(""), + err: true, + }, + { + scenario: "Request returns MagicAuth by ID", + client: NewClient("test"), + options: GetMagicAuthOpts{MagicAuth: "magic_auth_123"}, + expected: MagicAuth{ + ID: "magic_auth_123", + UserId: "user_123", + Email: "marcelina@foo-corp.com", + ExpiresAt: "2021-06-25T19:07:33.155Z", + Code: "123456", + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", + }, + }, + } + + for _, test := range tests { + t.Run(test.scenario, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(getMagicAuthTestHandler)) + defer server.Close() + + client := test.client + client.Endpoint = server.URL + client.HTTPClient = server.Client() + + magicAuth, err := client.GetMagicAuth(context.Background(), test.options) + if test.err { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, test.expected, magicAuth) + }) + } +} + +func getMagicAuthTestHandler(w http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + if auth != "Bearer test" { + http.Error(w, "bad auth", http.StatusUnauthorized) + return + } + + var body []byte + var err error + + if r.URL.Path == "/user_management/magic_auth/magic_auth_123" { + magicAuth := MagicAuth{ + ID: "magic_auth_123", + UserId: "user_123", + Email: "marcelina@foo-corp.com", + ExpiresAt: "2021-06-25T19:07:33.155Z", + Code: "123456", + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", + } + body, err = json.Marshal(magicAuth) + } + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Write(body) +} + +func TestCreateMagicAuth(t *testing.T) { + tests := []struct { + scenario string + client *Client + options CreateMagicAuthOpts + expected MagicAuth + err bool + }{ + { + scenario: "Request without API Key returns an error", + client: NewClient(""), + err: true, + }, + { + scenario: "Request returns MagicAuth", + client: NewClient("test"), + options: CreateMagicAuthOpts{ + Email: "marcelina@foo-corp.com", + }, + expected: MagicAuth{ + ID: "magic_auth_123", + UserId: "user_123", + Email: "marcelina@foo-corp.com", + ExpiresAt: "2021-06-25T19:07:33.155Z", + Code: "123456", + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", + }, + }, + } + + for _, test := range tests { + t.Run(test.scenario, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(CreateMagicAuthTestHandler)) + defer server.Close() + + client := test.client + client.Endpoint = server.URL + client.HTTPClient = server.Client() + + magicAuth, err := client.CreateMagicAuth(context.Background(), test.options) + if test.err { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, test.expected, magicAuth) + }) + } +} + +func CreateMagicAuthTestHandler(w http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + if auth != "Bearer test" { + http.Error(w, "bad auth", http.StatusUnauthorized) + return + } + + var body []byte + var err error + + if r.URL.Path == "/user_management/magic_auth" { + body, err = json.Marshal( + MagicAuth{ + ID: "magic_auth_123", + UserId: "user_123", + Email: "marcelina@foo-corp.com", + ExpiresAt: "2021-06-25T19:07:33.155Z", + Code: "123456", + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", + }) + } + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Write(body) +} + func TestSendMagicAuthCode(t *testing.T) { tests := []struct { scenario string @@ -1477,7 +2107,328 @@ func TestListAuthFactor(t *testing.T) { } } -func listAuthFactorsTestHandler(w http.ResponseWriter, r *http.Request) { +func listAuthFactorsTestHandler(w http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + if auth != "Bearer test" { + http.Error(w, "bad auth", http.StatusUnauthorized) + return + } + + var body []byte + var err error + + if r.URL.Path == "/user_management/users/user_01E3JC5F5Z1YJNPGVYWV9SX6GH/auth_factors" { + body, err = json.Marshal(ListAuthFactorsResponse{ + Data: []mfa.Factor{ + { + ID: "auth_factor_test123", + CreatedAt: "2022-02-17T22:39:26.616Z", + UpdatedAt: "2022-02-17T22:39:26.616Z", + Type: "generic_otp", + }, + { + ID: "auth_factor_test234", + CreatedAt: "2022-02-17T22:39:26.616Z", + UpdatedAt: "2022-02-17T22:39:26.616Z", + Type: "generic_otp", + }, + }, + }) + } + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Write(body) +} + +func TestGetOrganizationMembership(t *testing.T) { + tests := []struct { + scenario string + client *Client + options GetOrganizationMembershipOpts + expected OrganizationMembership + err bool + }{ + { + scenario: "Request without API Key returns an error", + client: NewClient(""), + err: true, + }, + { + scenario: "Request returns an Organization Membership", + client: NewClient("test"), + options: GetOrganizationMembershipOpts{ + OrganizationMembership: "om_01E4ZCR3C56J083X43JQXF3JK5", + }, + expected: OrganizationMembership{ + ID: "om_01E4ZCR3C56J083X43JQXF3JK5", + UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", + OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5", + Status: Active, + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", + }, + }, + } + + for _, test := range tests { + t.Run(test.scenario, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(getOrganizationMembershipTestHandler)) + defer server.Close() + + client := test.client + client.Endpoint = server.URL + client.HTTPClient = server.Client() + + organizationMembership, err := client.GetOrganizationMembership(context.Background(), test.options) + if test.err { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, test.expected, organizationMembership) + }) + } +} + +func getOrganizationMembershipTestHandler(w http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + if auth != "Bearer test" { + http.Error(w, "bad auth", http.StatusUnauthorized) + return + } + + var body []byte + var err error + + if r.URL.Path == "/user_management/organization_memberships/om_01E4ZCR3C56J083X43JQXF3JK5" { + body, err = json.Marshal(OrganizationMembership{ + ID: "om_01E4ZCR3C56J083X43JQXF3JK5", + UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", + OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5", + Status: Active, + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", + }) + } + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Write(body) +} + +func TestListOrganizationMemberships(t *testing.T) { + t.Run("ListOrganizationMemberships succeeds to fetch OrganizationMemberships belonging to an Organization", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(listOrganizationMembershipsTestHandler)) + defer server.Close() + client := &Client{ + HTTPClient: server.Client(), + Endpoint: server.URL, + APIKey: "test", + } + + expectedResponse := ListOrganizationMembershipsResponse{ + Data: []OrganizationMembership{ + { + ID: "om_01E4ZCR3C56J083X43JQXF3JK5", + UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", + OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5", + Status: Active, + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", + }, + }, + ListMetadata: common.ListMetadata{ + After: "", + }, + } + + organizationMemberships, err := client.ListOrganizationMemberships( + context.Background(), + ListOrganizationMembershipsOpts{OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5"}, + ) + + require.NoError(t, err) + require.Equal(t, expectedResponse, organizationMemberships) + }) + + t.Run("ListOrganizationMemberships succeeds to fetch OrganizationMemberships belonging to a User", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(listOrganizationMembershipsTestHandler)) + defer server.Close() + client := &Client{ + HTTPClient: server.Client(), + Endpoint: server.URL, + APIKey: "test", + } + + expectedResponse := ListOrganizationMembershipsResponse{ + Data: []OrganizationMembership{ + { + ID: "om_01E4ZCR3C56J083X43JQXF3JK5", + UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", + OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5", + Status: Active, + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", + }, + }, + ListMetadata: common.ListMetadata{ + After: "", + }, + } + + organizationMemberships, err := client.ListOrganizationMemberships( + context.Background(), + ListOrganizationMembershipsOpts{UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E"}, + ) + + require.NoError(t, err) + require.Equal(t, expectedResponse, organizationMemberships) + }) + + t.Run("ListOrganizationMemberships succeeds to fetch OrganizationMemberships belonging to a User with particular statuses", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(listOrganizationMembershipsTestHandler)) + defer server.Close() + client := &Client{ + HTTPClient: server.Client(), + Endpoint: server.URL, + APIKey: "test", + } + + expectedResponse := ListOrganizationMembershipsResponse{ + Data: []OrganizationMembership{ + { + ID: "om_01E4ZCR3C56J083X43JQXF3JK5", + UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", + OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5", + Status: Active, + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", + }, + }, + ListMetadata: common.ListMetadata{ + After: "", + }, + } + + organizationMemberships, err := client.ListOrganizationMemberships( + context.Background(), + ListOrganizationMembershipsOpts{Statuses: []OrganizationMembershipStatus{Active, Inactive}, UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E"}, + ) + + require.NoError(t, err) + require.Equal(t, expectedResponse, organizationMemberships) + }) +} + +func listOrganizationMembershipsTestHandler(w http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + if auth != "Bearer test" { + http.Error(w, "bad auth", http.StatusUnauthorized) + return + } + + if userAgent := r.Header.Get("User-Agent"); !strings.Contains(userAgent, "workos-go/") { + w.WriteHeader(http.StatusBadRequest) + return + } + + var body []byte + var err error + + if r.URL.Path == "/user_management/organization_memberships" { + body, err = json.Marshal(struct { + ListOrganizationMembershipsResponse + }{ + ListOrganizationMembershipsResponse: ListOrganizationMembershipsResponse{ + Data: []OrganizationMembership{ + { + ID: "om_01E4ZCR3C56J083X43JQXF3JK5", + UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", + OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5", + Status: Active, + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", + }, + }, + ListMetadata: common.ListMetadata{ + After: "", + }, + }, + }) + } + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Write(body) +} + +func TestCreateOrganizationMembership(t *testing.T) { + tests := []struct { + scenario string + client *Client + options CreateOrganizationMembershipOpts + expected OrganizationMembership + err bool + }{ + { + scenario: "Request without API Key returns an error", + client: NewClient(""), + err: true, + }, + { + scenario: "Request returns OrganizationMembership", + client: NewClient("test"), + options: CreateOrganizationMembershipOpts{ + UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", + OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5", + }, + expected: OrganizationMembership{ + ID: "om_01E4ZCR3C56J083X43JQXF3JK5", + UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", + OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5", + Status: Active, + Role: common.RoleResponse{ + Slug: "member", + }, + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", + }, + }, + } + + for _, test := range tests { + t.Run(test.scenario, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(createOrganizationMembershipTestHandler)) + defer server.Close() + + client := test.client + client.Endpoint = server.URL + client.HTTPClient = server.Client() + + user, err := client.CreateOrganizationMembership(context.Background(), test.options) + if test.err { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, test.expected, user) + }) + } +} + +func createOrganizationMembershipTestHandler(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization") if auth != "Bearer test" { http.Error(w, "bad auth", http.StatusUnauthorized) @@ -1487,22 +2438,17 @@ func listAuthFactorsTestHandler(w http.ResponseWriter, r *http.Request) { var body []byte var err error - if r.URL.Path == "/user_management/users/user_01E3JC5F5Z1YJNPGVYWV9SX6GH/auth_factors" { - body, err = json.Marshal(ListAuthFactorsResponse{ - Data: []mfa.Factor{ - { - ID: "auth_factor_test123", - CreatedAt: "2022-02-17T22:39:26.616Z", - UpdatedAt: "2022-02-17T22:39:26.616Z", - Type: "generic_otp", - }, - { - ID: "auth_factor_test234", - CreatedAt: "2022-02-17T22:39:26.616Z", - UpdatedAt: "2022-02-17T22:39:26.616Z", - Type: "generic_otp", - }, + if r.URL.Path == "/user_management/organization_memberships" { + body, err = json.Marshal(OrganizationMembership{ + ID: "om_01E4ZCR3C56J083X43JQXF3JK5", + UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", + OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5", + Status: Active, + Role: common.RoleResponse{ + Slug: "member", }, + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", }) } @@ -1515,13 +2461,14 @@ func listAuthFactorsTestHandler(w http.ResponseWriter, r *http.Request) { w.Write(body) } -func TestGetOrganizationMembership(t *testing.T) { +func TestUpdateOrganizationMembership(t *testing.T) { tests := []struct { - scenario string - client *Client - options GetOrganizationMembershipOpts - expected OrganizationMembership - err bool + scenario string + client *Client + organizationMembershipId string + options UpdateOrganizationMembershipOpts + expected OrganizationMembership + err bool }{ { scenario: "Request without API Key returns an error", @@ -1529,42 +2476,47 @@ func TestGetOrganizationMembership(t *testing.T) { err: true, }, { - scenario: "Request returns an Organization Membership", - client: NewClient("test"), - options: GetOrganizationMembershipOpts{ - OrganizationMembership: "om_01E4ZCR3C56J083X43JQXF3JK5", + scenario: "Request returns OrganizationMembership", + client: NewClient("test"), + organizationMembershipId: "om_01E4ZCR3C56J083X43JQXF3JK5", + options: UpdateOrganizationMembershipOpts{ + RoleSlug: "member", }, expected: OrganizationMembership{ ID: "om_01E4ZCR3C56J083X43JQXF3JK5", UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5", - CreatedAt: "2021-06-25T19:07:33.155Z", - UpdatedAt: "2021-06-25T19:07:33.155Z", + Status: Active, + Role: common.RoleResponse{ + Slug: "member", + }, + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", }, }, } for _, test := range tests { t.Run(test.scenario, func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(getOrganizationMembershipTestHandler)) + server := httptest.NewServer(http.HandlerFunc(updateOrganizationMembershipTestHandler)) defer server.Close() client := test.client client.Endpoint = server.URL client.HTTPClient = server.Client() - organizationMembership, err := client.GetOrganizationMembership(context.Background(), test.options) + body, err := client.UpdateOrganizationMembership(context.Background(), test.organizationMembershipId, test.options) if test.err { require.Error(t, err) return } require.NoError(t, err) - require.Equal(t, test.expected, organizationMembership) + require.Equal(t, test.expected, body) }) } } -func getOrganizationMembershipTestHandler(w http.ResponseWriter, r *http.Request) { +func updateOrganizationMembershipTestHandler(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization") if auth != "Bearer test" { http.Error(w, "bad auth", http.StatusUnauthorized) @@ -1579,8 +2531,12 @@ func getOrganizationMembershipTestHandler(w http.ResponseWriter, r *http.Request ID: "om_01E4ZCR3C56J083X43JQXF3JK5", UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5", - CreatedAt: "2021-06-25T19:07:33.155Z", - UpdatedAt: "2021-06-25T19:07:33.155Z", + Status: Active, + Role: common.RoleResponse{ + Slug: "member", + }, + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", }) } @@ -1593,109 +2549,63 @@ func getOrganizationMembershipTestHandler(w http.ResponseWriter, r *http.Request w.Write(body) } -func TestListOrganizationMemberships(t *testing.T) { - t.Run("ListOrganizationMemberships succeeds to fetch OrganizationMemberships belonging to an Organization", func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(listOrganizationMembershipsTestHandler)) - defer server.Close() - client := &Client{ - HTTPClient: server.Client(), - Endpoint: server.URL, - APIKey: "test", - } - - expectedResponse := ListOrganizationMembershipsResponse{ - Data: []OrganizationMembership{ - { - ID: "om_01E4ZCR3C56J083X43JQXF3JK5", - UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", - OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5", - CreatedAt: "2021-06-25T19:07:33.155Z", - UpdatedAt: "2021-06-25T19:07:33.155Z", - }, - }, - ListMetadata: common.ListMetadata{ - After: "", +func TestDeleteOrganizationMembership(t *testing.T) { + tests := []struct { + scenario string + client *Client + options DeleteOrganizationMembershipOpts + expected error + err bool + }{ + { + scenario: "Request without API Key returns an error", + client: NewClient(""), + err: true, + }, + { + scenario: "Request returns OrganizationMembership", + client: NewClient("test"), + options: DeleteOrganizationMembershipOpts{ + OrganizationMembership: "om_01E4ZCR3C56J083X43JQXF3JK5", }, - } - - organizationMemberships, err := client.ListOrganizationMemberships( - context.Background(), - ListOrganizationMembershipsOpts{OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5"}, - ) - - require.NoError(t, err) - require.Equal(t, expectedResponse, organizationMemberships) - }) - - t.Run("ListOrganizationMemberships succeeds to fetch OrganizationMemberships belonging to a User", func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(listOrganizationMembershipsTestHandler)) - defer server.Close() - client := &Client{ - HTTPClient: server.Client(), - Endpoint: server.URL, - APIKey: "test", - } + expected: nil, + }, + } - expectedResponse := ListOrganizationMembershipsResponse{ - Data: []OrganizationMembership{ - { - ID: "om_01E4ZCR3C56J083X43JQXF3JK5", - UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", - OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5", - CreatedAt: "2021-06-25T19:07:33.155Z", - UpdatedAt: "2021-06-25T19:07:33.155Z", - }, - }, - ListMetadata: common.ListMetadata{ - After: "", - }, - } + for _, test := range tests { + t.Run(test.scenario, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(deleteOrganizationMembershipTestHandler)) + defer server.Close() - organizationMemberships, err := client.ListOrganizationMemberships( - context.Background(), - ListOrganizationMembershipsOpts{UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E"}, - ) + client := test.client + client.Endpoint = server.URL + client.HTTPClient = server.Client() - require.NoError(t, err) - require.Equal(t, expectedResponse, organizationMemberships) - }) + err := client.DeleteOrganizationMembership(context.Background(), test.options) + if test.err { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, test.expected, err) + }) + } } -func listOrganizationMembershipsTestHandler(w http.ResponseWriter, r *http.Request) { +func deleteOrganizationMembershipTestHandler(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization") if auth != "Bearer test" { http.Error(w, "bad auth", http.StatusUnauthorized) return } - if userAgent := r.Header.Get("User-Agent"); !strings.Contains(userAgent, "workos-go/") { - w.WriteHeader(http.StatusBadRequest) - return - } - var body []byte var err error - if r.URL.Path == "/user_management/organization_memberships" { - body, err = json.Marshal(struct { - ListOrganizationMembershipsResponse - }{ - ListOrganizationMembershipsResponse: ListOrganizationMembershipsResponse{ - Data: []OrganizationMembership{ - { - ID: "om_01E4ZCR3C56J083X43JQXF3JK5", - UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", - OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5", - CreatedAt: "2021-06-25T19:07:33.155Z", - UpdatedAt: "2021-06-25T19:07:33.155Z", - }, - }, - ListMetadata: common.ListMetadata{ - After: "", - }, - }, - }) + if r.URL.Path == "/user_management/organization_memberships/om_01E4ZCR3C56J083X43JQXF3JK5" { + body, err = nil, nil } + if err != nil { w.WriteHeader(http.StatusInternalServerError) return @@ -1705,11 +2615,11 @@ func listOrganizationMembershipsTestHandler(w http.ResponseWriter, r *http.Reque w.Write(body) } -func TestCreateOrganizationMembership(t *testing.T) { +func TestDeactivateOrganizationMembership(t *testing.T) { tests := []struct { scenario string client *Client - options CreateOrganizationMembershipOpts + options DeactivateOrganizationMembershipOpts expected OrganizationMembership err bool }{ @@ -1719,16 +2629,16 @@ func TestCreateOrganizationMembership(t *testing.T) { err: true, }, { - scenario: "Request returns OrganizationMembership", + scenario: "Request returns an Organization Membership", client: NewClient("test"), - options: CreateOrganizationMembershipOpts{ - UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", - OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5", + options: DeactivateOrganizationMembershipOpts{ + OrganizationMembership: "om_01E4ZCR3C56J083X43JQXF3JK5", }, expected: OrganizationMembership{ ID: "om_01E4ZCR3C56J083X43JQXF3JK5", UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5", + Status: Inactive, CreatedAt: "2021-06-25T19:07:33.155Z", UpdatedAt: "2021-06-25T19:07:33.155Z", }, @@ -1737,25 +2647,25 @@ func TestCreateOrganizationMembership(t *testing.T) { for _, test := range tests { t.Run(test.scenario, func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(createOrganizationMembershipTestHandler)) + server := httptest.NewServer(http.HandlerFunc(deactivateOrganizationMembershipTestHandler)) defer server.Close() client := test.client client.Endpoint = server.URL client.HTTPClient = server.Client() - user, err := client.CreateOrganizationMembership(context.Background(), test.options) + organizationMembership, err := client.DeactivateOrganizationMembership(context.Background(), test.options) if test.err { require.Error(t, err) return } require.NoError(t, err) - require.Equal(t, test.expected, user) + require.Equal(t, test.expected, organizationMembership) }) } } -func createOrganizationMembershipTestHandler(w http.ResponseWriter, r *http.Request) { +func deactivateOrganizationMembershipTestHandler(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization") if auth != "Bearer test" { http.Error(w, "bad auth", http.StatusUnauthorized) @@ -1765,11 +2675,12 @@ func createOrganizationMembershipTestHandler(w http.ResponseWriter, r *http.Requ var body []byte var err error - if r.URL.Path == "/user_management/organization_memberships" { + if r.URL.Path == "/user_management/organization_memberships/om_01E4ZCR3C56J083X43JQXF3JK5/deactivate" { body, err = json.Marshal(OrganizationMembership{ ID: "om_01E4ZCR3C56J083X43JQXF3JK5", UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5", + Status: Inactive, CreatedAt: "2021-06-25T19:07:33.155Z", UpdatedAt: "2021-06-25T19:07:33.155Z", }) @@ -1784,12 +2695,12 @@ func createOrganizationMembershipTestHandler(w http.ResponseWriter, r *http.Requ w.Write(body) } -func TestDeleteOrganizationMembership(t *testing.T) { +func TestReactivateOrganizationMembership(t *testing.T) { tests := []struct { scenario string client *Client - options DeleteOrganizationMembershipOpts - expected error + options ReactivateOrganizationMembershipOpts + expected OrganizationMembership err bool }{ { @@ -1798,36 +2709,43 @@ func TestDeleteOrganizationMembership(t *testing.T) { err: true, }, { - scenario: "Request returns OrganizationMembership", + scenario: "Request returns an Organization Membership", client: NewClient("test"), - options: DeleteOrganizationMembershipOpts{ + options: ReactivateOrganizationMembershipOpts{ OrganizationMembership: "om_01E4ZCR3C56J083X43JQXF3JK5", }, - expected: nil, + expected: OrganizationMembership{ + ID: "om_01E4ZCR3C56J083X43JQXF3JK5", + UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", + OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5", + Status: Active, + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", + }, }, } for _, test := range tests { t.Run(test.scenario, func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(deleteOrganizationMembershipTestHandler)) + server := httptest.NewServer(http.HandlerFunc(reactivateOrganizationMembershipTestHandler)) defer server.Close() client := test.client client.Endpoint = server.URL client.HTTPClient = server.Client() - err := client.DeleteOrganizationMembership(context.Background(), test.options) + organizationMembership, err := client.ReactivateOrganizationMembership(context.Background(), test.options) if test.err { require.Error(t, err) return } require.NoError(t, err) - require.Equal(t, test.expected, err) + require.Equal(t, test.expected, organizationMembership) }) } } -func deleteOrganizationMembershipTestHandler(w http.ResponseWriter, r *http.Request) { +func reactivateOrganizationMembershipTestHandler(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization") if auth != "Bearer test" { http.Error(w, "bad auth", http.StatusUnauthorized) @@ -1837,8 +2755,15 @@ func deleteOrganizationMembershipTestHandler(w http.ResponseWriter, r *http.Requ var body []byte var err error - if r.URL.Path == "/user_management/organization_memberships/om_01E4ZCR3C56J083X43JQXF3JK5" { - body, err = nil, nil + if r.URL.Path == "/user_management/organization_memberships/om_01E4ZCR3C56J083X43JQXF3JK5/reactivate" { + body, err = json.Marshal(OrganizationMembership{ + ID: "om_01E4ZCR3C56J083X43JQXF3JK5", + UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", + OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5", + Status: Active, + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", + }) } if err != nil { @@ -1868,13 +2793,14 @@ func TestGetInvitation(t *testing.T) { client: NewClient("test"), options: GetInvitationOpts{Invitation: "invitation_123"}, expected: Invitation{ - ID: "invitation_123", - Email: "marcelina@foo-corp.com", - State: Pending, - Token: "myToken", - ExpiresAt: "2021-06-25T19:07:33.155Z", - CreatedAt: "2021-06-25T19:07:33.155Z", - UpdatedAt: "2021-06-25T19:07:33.155Z", + ID: "invitation_123", + Email: "marcelina@foo-corp.com", + State: Pending, + Token: "myToken", + AcceptInvitationUrl: "https://your-app.com/invite?invitation_token=myToken", + ExpiresAt: "2021-06-25T19:07:33.155Z", + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", }, }, } @@ -1911,13 +2837,14 @@ func getInvitationTestHandler(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/user_management/invitations/invitation_123" { invitations := Invitation{ - ID: "invitation_123", - Email: "marcelina@foo-corp.com", - State: Pending, - Token: "myToken", - ExpiresAt: "2021-06-25T19:07:33.155Z", - CreatedAt: "2021-06-25T19:07:33.155Z", - UpdatedAt: "2021-06-25T19:07:33.155Z", + ID: "invitation_123", + Email: "marcelina@foo-corp.com", + State: Pending, + Token: "myToken", + AcceptInvitationUrl: "https://your-app.com/invite?invitation_token=myToken", + ExpiresAt: "2021-06-25T19:07:33.155Z", + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", } body, err = json.Marshal(invitations) } @@ -1953,13 +2880,14 @@ func TestListInvitations(t *testing.T) { expected: ListInvitationsResponse{ Data: []Invitation{ { - ID: "invitation_123", - Email: "marcelina@foo-corp.com", - State: Pending, - Token: "myToken", - ExpiresAt: "2021-06-25T19:07:33.155Z", - CreatedAt: "2021-06-25T19:07:33.155Z", - UpdatedAt: "2021-06-25T19:07:33.155Z", + ID: "invitation_123", + Email: "marcelina@foo-corp.com", + State: Pending, + Token: "myToken", + AcceptInvitationUrl: "https://your-app.com/invite?invitation_token=myToken", + ExpiresAt: "2021-06-25T19:07:33.155Z", + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", }, }, ListMetadata: common.ListMetadata{ @@ -2003,13 +2931,14 @@ func listInvitationsTestHandler(w http.ResponseWriter, r *http.Request) { invitations := ListInvitationsResponse{ Data: []Invitation{ { - ID: "invitation_123", - Email: "marcelina@foo-corp.com", - State: Pending, - Token: "myToken", - ExpiresAt: "2021-06-25T19:07:33.155Z", - CreatedAt: "2021-06-25T19:07:33.155Z", - UpdatedAt: "2021-06-25T19:07:33.155Z", + ID: "invitation_123", + Email: "marcelina@foo-corp.com", + State: Pending, + Token: "myToken", + AcceptInvitationUrl: "https://your-app.com/invite?invitation_token=myToken", + ExpiresAt: "2021-06-25T19:07:33.155Z", + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", }, }, ListMetadata: common.ListMetadata{ @@ -2049,15 +2978,17 @@ func TestSendInvitation(t *testing.T) { OrganizationID: "org_123", ExpiresInDays: 7, InviterUserID: "user_123", + RoleSlug: "admin", }, expected: Invitation{ - ID: "invitation_123", - Email: "marcelina@foo-corp.com", - State: Pending, - Token: "myToken", - ExpiresAt: "2021-06-25T19:07:33.155Z", - CreatedAt: "2021-06-25T19:07:33.155Z", - UpdatedAt: "2021-06-25T19:07:33.155Z", + ID: "invitation_123", + Email: "marcelina@foo-corp.com", + State: Pending, + Token: "myToken", + AcceptInvitationUrl: "https://your-app.com/invite?invitation_token=myToken", + ExpiresAt: "2021-06-25T19:07:33.155Z", + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", }, }, } @@ -2095,13 +3026,14 @@ func SendInvitationTestHandler(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/user_management/invitations" { body, err = json.Marshal( Invitation{ - ID: "invitation_123", - Email: "marcelina@foo-corp.com", - State: Pending, - Token: "myToken", - ExpiresAt: "2021-06-25T19:07:33.155Z", - CreatedAt: "2021-06-25T19:07:33.155Z", - UpdatedAt: "2021-06-25T19:07:33.155Z", + ID: "invitation_123", + Email: "marcelina@foo-corp.com", + State: Pending, + Token: "myToken", + AcceptInvitationUrl: "https://your-app.com/invite?invitation_token=myToken", + ExpiresAt: "2021-06-25T19:07:33.155Z", + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", }) } @@ -2135,13 +3067,14 @@ func TestRevokeInvitation(t *testing.T) { }, expected: Invitation{ - ID: "invitation_123", - Email: "marcelina@foo-corp.com", - State: Pending, - Token: "myToken", - ExpiresAt: "2021-06-25T19:07:33.155Z", - CreatedAt: "2021-06-25T19:07:33.155Z", - UpdatedAt: "2021-06-25T19:07:33.155Z", + ID: "invitation_123", + Email: "marcelina@foo-corp.com", + State: Pending, + Token: "myToken", + AcceptInvitationUrl: "https://your-app.com/invite?invitation_token=myToken", + ExpiresAt: "2021-06-25T19:07:33.155Z", + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", }, }, } @@ -2179,13 +3112,14 @@ func RevokeInvitationTestHandler(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/user_management/invitations/invitation_123/revoke" { body, err = json.Marshal( Invitation{ - ID: "invitation_123", - Email: "marcelina@foo-corp.com", - State: Pending, - Token: "myToken", - ExpiresAt: "2021-06-25T19:07:33.155Z", - CreatedAt: "2021-06-25T19:07:33.155Z", - UpdatedAt: "2021-06-25T19:07:33.155Z", + ID: "invitation_123", + Email: "marcelina@foo-corp.com", + State: Pending, + Token: "myToken", + AcceptInvitationUrl: "https://your-app.com/invite?invitation_token=myToken", + ExpiresAt: "2021-06-25T19:07:33.155Z", + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", }) } @@ -2197,3 +3131,16 @@ func RevokeInvitationTestHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write(body) } + +func RevokeSessionTestHandler(w http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + if auth != "Bearer test" { + http.Error(w, "bad auth", http.StatusUnauthorized) + return + } + + var body []byte + + w.WriteHeader(http.StatusOK) + w.Write(body) +} diff --git a/pkg/usermanagement/usermanagement.go b/pkg/usermanagement/usermanagement.go index 39e59fc4..1a60ab97 100644 --- a/pkg/usermanagement/usermanagement.go +++ b/pkg/usermanagement/usermanagement.go @@ -86,7 +86,7 @@ func GetAuthorizationURL(opts GetAuthorizationURLOpts) (*url.URL, error) { return DefaultClient.GetAuthorizationURL(opts) } -// AuthenticateWithPassword authenticates a user with email and password and optionally creates a session. +// AuthenticateWithPassword authenticates a user with email and password func AuthenticateWithPassword( ctx context.Context, opts AuthenticateWithPasswordOpts, @@ -94,8 +94,7 @@ func AuthenticateWithPassword( return DefaultClient.AuthenticateWithPassword(ctx, opts) } -// AuthenticateWithCode authenticates an OAuth user or a managed SSO user that is logging in through SSO, and -// optionally creates a session. +// AuthenticateWithCode authenticates an OAuth user or a managed SSO user that is logging in through SSO func AuthenticateWithCode( ctx context.Context, opts AuthenticateWithCodeOpts, @@ -103,6 +102,15 @@ func AuthenticateWithCode( return DefaultClient.AuthenticateWithCode(ctx, opts) } +// AuthenticateWithRefreshToken obtains a new AccessToken and RefreshToken for +// an existing session +func AuthenticateWithRefreshToken( + ctx context.Context, + opts AuthenticateWithRefreshTokenOpts, +) (RefreshAuthenticationResponse, error) { + return DefaultClient.AuthenticateWithRefreshToken(ctx, opts) +} + // AuthenticateWithMagicAuth authenticates a user by verifying a one-time code sent to the user's email address by // the Magic Auth Send Code endpoint. func AuthenticateWithMagicAuth( @@ -136,6 +144,14 @@ func AuthenticateWithOrganizationSelection( return DefaultClient.AuthenticateWithOrganizationSelection(ctx, opts) } +// GetEmailVerification fetches an EmailVerification object by its ID. +func GetEmailVerification( + ctx context.Context, + opts GetEmailVerificationOpts, +) (EmailVerification, error) { + return DefaultClient.GetEmailVerification(ctx, opts) +} + // SendVerificationEmail creates an email verification challenge and emails verification token to user. func SendVerificationEmail( ctx context.Context, @@ -152,7 +168,23 @@ func VerifyEmail( return DefaultClient.VerifyEmail(ctx, opts) } -// SendPasswordResetEmail creates a password reset challenge and emails a password reset link to an unmanaged user. +// GetPasswordReset fetches a Password Reset object by its ID. +func GetPasswordReset( + ctx context.Context, + opts GetPasswordResetOpts, +) (PasswordReset, error) { + return DefaultClient.GetPasswordReset(ctx, opts) +} + +// CreatePasswordReset creates a password reset token that can be sent to the user's email address and used to reset the password. +func CreatePasswordReset( + ctx context.Context, + opts CreatePasswordResetOpts, +) (PasswordReset, error) { + return DefaultClient.CreatePasswordReset(ctx, opts) +} + +// Deprecated: Use CreatePasswordReset instead. This method will be removed in a future major version. func SendPasswordResetEmail( ctx context.Context, opts SendPasswordResetEmailOpts, @@ -168,7 +200,23 @@ func ResetPassword( return DefaultClient.ResetPassword(ctx, opts) } -// SendMagicAuthCode sends a one-time code to the user's email address. +// GetMagicAuth fetches a Magic Auth object by its ID. +func GetMagicAuth( + ctx context.Context, + opts GetMagicAuthOpts, +) (MagicAuth, error) { + return DefaultClient.GetMagicAuth(ctx, opts) +} + +// CreateMagicAuth creates a one-time code that can be sent to the user's email address and used for authentication. +func CreateMagicAuth( + ctx context.Context, + opts CreateMagicAuthOpts, +) (MagicAuth, error) { + return DefaultClient.CreateMagicAuth(ctx, opts) +} + +// Deprecated: Use CreateMagicAuth instead. This method will be removed in a future major version. func SendMagicAuthCode( ctx context.Context, opts SendMagicAuthCodeOpts, @@ -208,7 +256,7 @@ func ListOrganizationMemberships( return DefaultClient.ListOrganizationMemberships(ctx, opts) } -// CreateOrganizationMembership creates a OrganizationMembership. +// CreateOrganizationMembership creates an OrganizationMembership. func CreateOrganizationMembership( ctx context.Context, opts CreateOrganizationMembershipOpts, @@ -216,7 +264,16 @@ func CreateOrganizationMembership( return DefaultClient.CreateOrganizationMembership(ctx, opts) } -// DeleteOrganizationMembership deletes a existing OrganizationMembership. +// UpdateOrganizationMembership updates an OrganizationMembership. +func UpdateOrganizationMembership( + ctx context.Context, + organizationMembershipId string, + opts UpdateOrganizationMembershipOpts, +) (OrganizationMembership, error) { + return DefaultClient.UpdateOrganizationMembership(ctx, organizationMembershipId, opts) +} + +// DeleteOrganizationMembership deletes an existing OrganizationMembership. func DeleteOrganizationMembership( ctx context.Context, opts DeleteOrganizationMembershipOpts, @@ -224,6 +281,22 @@ func DeleteOrganizationMembership( return DefaultClient.DeleteOrganizationMembership(ctx, opts) } +// DeactivateOrganizationMembership deactivates an OrganizationMembership. +func DeactivateOrganizationMembership( + ctx context.Context, + opts DeactivateOrganizationMembershipOpts, +) (OrganizationMembership, error) { + return DefaultClient.DeactivateOrganizationMembership(ctx, opts) +} + +// ReactivateOrganizationMembership reactivates an OrganizationMembership. +func ReactivateOrganizationMembership( + ctx context.Context, + opts ReactivateOrganizationMembershipOpts, +) (OrganizationMembership, error) { + return DefaultClient.ReactivateOrganizationMembership(ctx, opts) +} + func GetInvitation( ctx context.Context, opts GetInvitationOpts, @@ -251,3 +324,15 @@ func RevokeInvitation( ) (Invitation, error) { return DefaultClient.RevokeInvitation(ctx, opts) } + +func GetJWKSURL(clientID string) (*url.URL, error) { + return DefaultClient.GetJWKSURL(clientID) +} + +func GetLogoutURL(opts GetLogoutURLOpts) (*url.URL, error) { + return DefaultClient.GetLogoutURL(opts) +} + +func RevokeSession(ctx context.Context, opts RevokeSessionOpts) error { + return DefaultClient.RevokeSession(ctx, opts) +} diff --git a/pkg/usermanagement/usermanagement_test.go b/pkg/usermanagement/usermanagement_test.go index 18e458a8..858bc7f8 100644 --- a/pkg/usermanagement/usermanagement_test.go +++ b/pkg/usermanagement/usermanagement_test.go @@ -107,6 +107,37 @@ func TestUserManagementCreateUser(t *testing.T) { require.Equal(t, expectedResponse, userRes) } +func TestUserManagementCreateUserPasswordHash(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(createUserTestHandler)) + defer server.Close() + + DefaultClient = mockClient(server) + + SetAPIKey("test") + + expectedResponse := User{ + ID: "user_01E3JC5F5Z1YJNPGVYWV9SX6GH", + Email: "marcelina@foo-corp.com", + FirstName: "Marcelina", + LastName: "Davis", + EmailVerified: true, + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", + } + + userRes, err := CreateUser(context.Background(), CreateUserOpts{ + Email: "marcelina@gmail.com", + FirstName: "Marcelina", + LastName: "Davis", + EmailVerified: true, + PasswordHash: "$2b$10$dXS6RadWKYIqs6vOwqKZceLuCIqz6S81t06.yOkGJbbfeO9go4fai", + PasswordHashType: "bcrypt", + }) + + require.NoError(t, err) + require.Equal(t, expectedResponse, userRes) +} + func TestUserManagementUpdateUser(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(updateUserTestHandler)) defer server.Close() @@ -184,6 +215,31 @@ func TestUsersDeleteUser(t *testing.T) { require.Equal(t, nil, err) } +func TestUsersGetEmailVerification(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(getEmailVerificationTestHandler)) + defer server.Close() + + DefaultClient = mockClient(server) + SetAPIKey("test") + + expectedResponse := EmailVerification{ + ID: "email_verification_123", + UserId: "user_123", + Email: "marcelina@foo-corp.com", + ExpiresAt: "2021-06-25T19:07:33.155Z", + Code: "123456", + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", + } + + getByIDRes, err := GetEmailVerification(context.Background(), GetEmailVerificationOpts{ + EmailVerification: "email_verification_123", + }) + + require.NoError(t, err) + require.Equal(t, expectedResponse, getByIDRes) +} + func TestUsersSendVerificationEmail(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(sendVerificationEmailTestHandler)) defer server.Close() @@ -239,6 +295,58 @@ func TestUserManagementVerifyEmail(t *testing.T) { require.Equal(t, expectedResponse, userRes) } +func TestUsersGetPasswordReset(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(getPasswordResetTestHandler)) + defer server.Close() + + DefaultClient = mockClient(server) + SetAPIKey("test") + + expectedResponse := PasswordReset{ + ID: "password_reset_123", + UserId: "user_123", + Email: "marcelina@foo-corp.com", + PasswordResetToken: "myToken", + PasswordResetUrl: "https://your-app.com/reset-password?token=myToken", + ExpiresAt: "2021-06-25T19:07:33.155Z", + CreatedAt: "2021-06-25T19:07:33.155Z", + } + + getByIDRes, err := GetPasswordReset(context.Background(), GetPasswordResetOpts{ + PasswordReset: "password_reset_123", + }) + + require.NoError(t, err) + require.Equal(t, expectedResponse, getByIDRes) +} + +func TestUserManagementCreatePasswordReset(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(CreatePasswordResetTestHandler)) + + defer server.Close() + + DefaultClient = mockClient(server) + + SetAPIKey("test") + + expectedResponse := PasswordReset{ + ID: "password_reset_123", + UserId: "user_123", + Email: "marcelina@foo-corp.com", + PasswordResetToken: "myToken", + PasswordResetUrl: "https://your-app.com/reset-password?token=myToken", + ExpiresAt: "2021-06-25T19:07:33.155Z", + CreatedAt: "2021-06-25T19:07:33.155Z", + } + + createRes, err := CreatePasswordReset(context.Background(), CreatePasswordResetOpts{ + Email: "marcelina@foo-corp.com", + }) + + require.NoError(t, err) + require.Equal(t, expectedResponse, createRes) +} + func TestUserManagementCreatePasswordResetChallenge(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(sendPasswordResetEmailTestHandler)) defer server.Close() @@ -316,6 +424,8 @@ func TestUserManagementAuthenticateWithCode(t *testing.T) { Email: "employee@foo-corp.com", }, OrganizationID: "org_123", + AccessToken: "access_token", + RefreshToken: "refresh_token", } authenticationRes, err := AuthenticateWithCode(context.Background(), AuthenticateWithCodeOpts{}) @@ -341,6 +451,8 @@ func TestUserManagementAuthenticateWithPassword(t *testing.T) { Email: "employee@foo-corp.com", }, OrganizationID: "org_123", + AccessToken: "access_token", + RefreshToken: "refresh_token", } authenticationRes, err := AuthenticateWithPassword(context.Background(), AuthenticateWithPasswordOpts{}) @@ -366,6 +478,8 @@ func TestUserManagementAuthenticateWithMagicAuth(t *testing.T) { Email: "employee@foo-corp.com", }, OrganizationID: "org_123", + AccessToken: "access_token", + RefreshToken: "refresh_token", } authenticationRes, err := AuthenticateWithMagicAuth(context.Background(), AuthenticateWithMagicAuthOpts{}) @@ -391,6 +505,8 @@ func TestUserManagementAuthenticateWithTOTP(t *testing.T) { Email: "employee@foo-corp.com", }, OrganizationID: "org_123", + AccessToken: "access_token", + RefreshToken: "refresh_token", } authenticationRes, err := AuthenticateWithTOTP(context.Background(), AuthenticateWithTOTPOpts{}) @@ -416,6 +532,8 @@ func TestUserManagementAuthenticateWithEmailVerificationCode(t *testing.T) { Email: "employee@foo-corp.com", }, OrganizationID: "org_123", + AccessToken: "access_token", + RefreshToken: "refresh_token", } authenticationRes, err := AuthenticateWithEmailVerificationCode(context.Background(), AuthenticateWithEmailVerificationCodeOpts{}) @@ -441,6 +559,8 @@ func TestUserManagementAuthenticateWithOrganizationSelection(t *testing.T) { Email: "employee@foo-corp.com", }, OrganizationID: "org_123", + AccessToken: "access_token", + RefreshToken: "refresh_token", } authenticationRes, err := AuthenticateWithOrganizationSelection(context.Background(), AuthenticateWithOrganizationSelectionOpts{}) @@ -449,6 +569,58 @@ func TestUserManagementAuthenticateWithOrganizationSelection(t *testing.T) { require.Equal(t, expectedResponse, authenticationRes) } +func TestUsersGetMagicAuth(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(getMagicAuthTestHandler)) + defer server.Close() + + DefaultClient = mockClient(server) + SetAPIKey("test") + + expectedResponse := MagicAuth{ + ID: "magic_auth_123", + UserId: "user_123", + Email: "marcelina@foo-corp.com", + ExpiresAt: "2021-06-25T19:07:33.155Z", + Code: "123456", + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", + } + + getByIDRes, err := GetMagicAuth(context.Background(), GetMagicAuthOpts{ + MagicAuth: "magic_auth_123", + }) + + require.NoError(t, err) + require.Equal(t, expectedResponse, getByIDRes) +} + +func TestUserManagementCreateMagicAuth(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(CreateMagicAuthTestHandler)) + + defer server.Close() + + DefaultClient = mockClient(server) + + SetAPIKey("test") + + expectedResponse := MagicAuth{ + ID: "magic_auth_123", + UserId: "user_123", + Email: "marcelina@foo-corp.com", + ExpiresAt: "2021-06-25T19:07:33.155Z", + Code: "123456", + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", + } + + createRes, err := CreateMagicAuth(context.Background(), CreateMagicAuthOpts{ + Email: "marcelina@foo-corp.com", + }) + + require.NoError(t, err) + require.Equal(t, expectedResponse, createRes) +} + func TestUserManagementSendMagicAuthCode(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(sendMagicAuthCodeTestHandler)) @@ -544,6 +716,7 @@ func TestUserManagementGetOrganizationMembership(t *testing.T) { ID: "om_01E4ZCR3C56J083X43JQXF3JK5", UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5", + Status: Active, CreatedAt: "2021-06-25T19:07:33.155Z", UpdatedAt: "2021-06-25T19:07:33.155Z", } @@ -571,6 +744,7 @@ func TestUserManagementListOrganizationMemberships(t *testing.T) { ID: "om_01E4ZCR3C56J083X43JQXF3JK5", UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5", + Status: Active, CreatedAt: "2021-06-25T19:07:33.155Z", UpdatedAt: "2021-06-25T19:07:33.155Z", }, @@ -594,10 +768,16 @@ func TestUserManagementCreateOrganizationMembership(t *testing.T) { SetAPIKey("test") + expectedRole := common.RoleResponse{ + Slug: "member", + } + expectedResponse := OrganizationMembership{ ID: "om_01E4ZCR3C56J083X43JQXF3JK5", UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5", + Status: Active, + Role: expectedRole, CreatedAt: "2021-06-25T19:07:33.155Z", UpdatedAt: "2021-06-25T19:07:33.155Z", } @@ -627,6 +807,90 @@ func TestUsersDeleteOrganizationMembership(t *testing.T) { require.Equal(t, nil, err) } +func TestUsersUpdateOrganizationMembership(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(updateOrganizationMembershipTestHandler)) + defer server.Close() + + DefaultClient = mockClient(server) + + SetAPIKey("test") + + expectedRole := common.RoleResponse{ + Slug: "member", + } + + expectedResponse := OrganizationMembership{ + ID: "om_01E4ZCR3C56J083X43JQXF3JK5", + UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", + OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5", + Status: Active, + Role: expectedRole, + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", + } + + body, err := UpdateOrganizationMembership( + context.Background(), + "om_01E4ZCR3C56J083X43JQXF3JK5", + UpdateOrganizationMembershipOpts{ + RoleSlug: "member", + }, + ) + + require.NoError(t, err) + require.Equal(t, expectedResponse, body) +} + +func TestUserManagementDeactivateOrganizationMembership(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(deactivateOrganizationMembershipTestHandler)) + defer server.Close() + + DefaultClient = mockClient(server) + + SetAPIKey("test") + + expectedResponse := OrganizationMembership{ + ID: "om_01E4ZCR3C56J083X43JQXF3JK5", + UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", + OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5", + Status: Inactive, + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", + } + + userRes, err := DeactivateOrganizationMembership(context.Background(), DeactivateOrganizationMembershipOpts{ + OrganizationMembership: "om_01E4ZCR3C56J083X43JQXF3JK5", + }) + + require.NoError(t, err) + require.Equal(t, expectedResponse, userRes) +} + +func TestUserManagementReactivateOrganizationMembership(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(reactivateOrganizationMembershipTestHandler)) + defer server.Close() + + DefaultClient = mockClient(server) + + SetAPIKey("test") + + expectedResponse := OrganizationMembership{ + ID: "om_01E4ZCR3C56J083X43JQXF3JK5", + UserID: "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", + OrganizationID: "org_01E4ZCR3C56J083X43JQXF3JK5", + Status: Active, + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", + } + + userRes, err := ReactivateOrganizationMembership(context.Background(), ReactivateOrganizationMembershipOpts{ + OrganizationMembership: "om_01E4ZCR3C56J083X43JQXF3JK5", + }) + + require.NoError(t, err) + require.Equal(t, expectedResponse, userRes) +} + func TestUsersGetInvitation(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(getInvitationTestHandler)) defer server.Close() @@ -635,13 +899,14 @@ func TestUsersGetInvitation(t *testing.T) { SetAPIKey("test") expectedResponse := Invitation{ - ID: "invitation_123", - Email: "marcelina@foo-corp.com", - State: Pending, - Token: "myToken", - ExpiresAt: "2021-06-25T19:07:33.155Z", - CreatedAt: "2021-06-25T19:07:33.155Z", - UpdatedAt: "2021-06-25T19:07:33.155Z", + ID: "invitation_123", + Email: "marcelina@foo-corp.com", + State: Pending, + Token: "myToken", + AcceptInvitationUrl: "https://your-app.com/invite?invitation_token=myToken", + ExpiresAt: "2021-06-25T19:07:33.155Z", + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", } getByIDRes, err := GetInvitation(context.Background(), GetInvitationOpts{ @@ -663,13 +928,14 @@ func TestUsersListInvitations(t *testing.T) { ListInvitationsResponse{ Data: []Invitation{ { - ID: "invitation_123", - Email: "marcelina@foo-corp.com", - State: Pending, - Token: "myToken", - ExpiresAt: "2021-06-25T19:07:33.155Z", - CreatedAt: "2021-06-25T19:07:33.155Z", - UpdatedAt: "2021-06-25T19:07:33.155Z", + ID: "invitation_123", + Email: "marcelina@foo-corp.com", + State: Pending, + Token: "myToken", + AcceptInvitationUrl: "https://your-app.com/invite?invitation_token=myToken", + ExpiresAt: "2021-06-25T19:07:33.155Z", + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", }, }, ListMetadata: common.ListMetadata{ @@ -695,13 +961,14 @@ func TestUsersSendInvitation(t *testing.T) { SetAPIKey("test") expectedResponse := Invitation{ - ID: "invitation_123", - Email: "marcelina@foo-corp.com", - State: Pending, - Token: "myToken", - ExpiresAt: "2021-06-25T19:07:33.155Z", - CreatedAt: "2021-06-25T19:07:33.155Z", - UpdatedAt: "2021-06-25T19:07:33.155Z", + ID: "invitation_123", + Email: "marcelina@foo-corp.com", + State: Pending, + Token: "myToken", + AcceptInvitationUrl: "https://your-app.com/invite?invitation_token=myToken", + ExpiresAt: "2021-06-25T19:07:33.155Z", + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", } createRes, err := SendInvitation(context.Background(), SendInvitationOpts{ @@ -709,6 +976,7 @@ func TestUsersSendInvitation(t *testing.T) { OrganizationID: "org_123", ExpiresInDays: 7, InviterUserID: "user_123", + RoleSlug: "admin", }) require.NoError(t, err) @@ -725,13 +993,14 @@ func TestUsersRevokeInvitation(t *testing.T) { SetAPIKey("test") expectedResponse := Invitation{ - ID: "invitation_123", - Email: "marcelina@foo-corp.com", - State: Pending, - Token: "myToken", - ExpiresAt: "2021-06-25T19:07:33.155Z", - CreatedAt: "2021-06-25T19:07:33.155Z", - UpdatedAt: "2021-06-25T19:07:33.155Z", + ID: "invitation_123", + Email: "marcelina@foo-corp.com", + State: Pending, + Token: "myToken", + AcceptInvitationUrl: "https://your-app.com/invite?invitation_token=myToken", + ExpiresAt: "2021-06-25T19:07:33.155Z", + CreatedAt: "2021-06-25T19:07:33.155Z", + UpdatedAt: "2021-06-25T19:07:33.155Z", } revokeRes, err := RevokeInvitation(context.Background(), RevokeInvitationOpts{ @@ -741,3 +1010,43 @@ func TestUsersRevokeInvitation(t *testing.T) { require.NoError(t, err) require.Equal(t, expectedResponse, revokeRes) } + +func TestUserManagementGetJWKSURL(t *testing.T) { + client := NewClient("test") + + u, err := client.GetJWKSURL("client_123") + require.NoError(t, err) + require.Equal( + t, + "https://api.workos.com/sso/jwks/client_123", + u.String(), + ) +} + +func TestUsersManagementGetLogoutURL(t *testing.T) { + client := NewClient("test") + + u, err := client.GetLogoutURL(GetLogoutURLOpts{SessionID: "session_abc"}) + require.NoError(t, err) + require.Equal( + t, + "https://api.workos.com/user_management/sessions/logout?session_id=session_abc", + u.String(), + ) +} + +func TestUsersRevokeSession(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(RevokeSessionTestHandler)) + + defer server.Close() + + DefaultClient = mockClient(server) + + SetAPIKey("test") + + err := RevokeSession(context.Background(), RevokeSessionOpts{ + SessionID: "session_123", + }) + + require.NoError(t, err) +}