diff --git a/mongodbatlas/organization_invitations.go b/mongodbatlas/organization_invitations.go new file mode 100644 index 000000000..c9de4b2e7 --- /dev/null +++ b/mongodbatlas/organization_invitations.go @@ -0,0 +1,196 @@ +// Copyright 2021 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mongodbatlas + +import ( + "context" + "fmt" + "net/http" +) + +const invitationBasePath = orgsBasePath + "/%s/invites" + +// InvitationOptions filtering options for invitations. +type InvitationOptions struct { + Username string `url:"username,omitempty"` +} + +// Invitation represents the structure of an Invitation. +type Invitation struct { + ID string `json:"id,omitempty"` + OrgID string `json:"orgId,omitempty"` + OrgName string `json:"orgName,omitempty"` + CreatedAt string `json:"createdAt,omitempty"` + ExpiresAt string `json:"expiresAt,omitempty"` + InviterUsername string `json:"inviterUsername,omitempty"` + Username string `json:"username,omitempty"` + Roles []string `json:"roles,omitempty"` + TeamIDs []string `json:"teamIds,omitempty"` +} + +// Invitations gets all unaccepted invitations to the specified Atlas organization. +// +// See more: https://docs.atlas.mongodb.com/reference/api/organization-get-invitations/ +func (s *OrganizationsServiceOp) Invitations(ctx context.Context, orgID string, opts *InvitationOptions) ([]*Invitation, *Response, error) { + if orgID == "" { + return nil, nil, NewArgError("orgID", "must be set") + } + + basePath := fmt.Sprintf(invitationBasePath, orgID) + path, err := setListOptions(basePath, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.Client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + var root []*Invitation + resp, err := s.Client.Do(ctx, req, &root) + if err != nil { + return nil, resp, err + } + + return root, resp, nil +} + +// Invitation gets details for one unaccepted invitation to the specified Atlas organization. +// +// See more: https://docs.atlas.mongodb.com/reference/api/organization-get-one-invitation/ +func (s *OrganizationsServiceOp) Invitation(ctx context.Context, orgID, invitationID string) (*Invitation, *Response, error) { + if orgID == "" { + return nil, nil, NewArgError("orgID", "must be set") + } + + if invitationID == "" { + return nil, nil, NewArgError("invitationID", "must be set") + } + + basePath := fmt.Sprintf(invitationBasePath, orgID) + path := fmt.Sprintf("%s/%s", basePath, invitationID) + + req, err := s.Client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(Invitation) + resp, err := s.Client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root, resp, nil +} + +// InviteUser invites one user to the Atlas organization that you specify. +// +// See more: https://docs.atlas.mongodb.com/reference/api/organization-create-one-invitation/ +func (s *OrganizationsServiceOp) InviteUser(ctx context.Context, invitation *Invitation) (*Invitation, *Response, error) { + if invitation.OrgID == "" { + return nil, nil, NewArgError("orgID", "must be set") + } + + path := fmt.Sprintf(invitationBasePath, invitation.OrgID) + + req, err := s.Client.NewRequest(ctx, http.MethodPost, path, invitation) + if err != nil { + return nil, nil, err + } + + root := new(Invitation) + resp, err := s.Client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root, resp, nil +} + +// UpdateInvitation updates one pending invitation to the Atlas organization that you specify. +// +// See more: https://docs.atlas.mongodb.com/reference/api/organization-update-one-invitation/ +func (s *OrganizationsServiceOp) UpdateInvitation(ctx context.Context, invitation *Invitation) (*Invitation, *Response, error) { + if invitation.OrgID == "" { + return nil, nil, NewArgError("orgID", "must be set") + } + + return s.updateInvitation(ctx, invitation) +} + +// UpdateInvitationByID updates one invitation to the Atlas organization. +// +// See more: https://docs.atlas.mongodb.com/reference/api/organization-update-one-invitation-by-id/ +func (s *OrganizationsServiceOp) UpdateInvitationByID(ctx context.Context, invitationID string, invitation *Invitation) (*Invitation, *Response, error) { + if invitation.OrgID == "" { + return nil, nil, NewArgError("orgID", "must be set") + } + + if invitationID == "" { + return nil, nil, NewArgError("invitationID", "must be set") + } + + invitation.ID = invitationID + + return s.updateInvitation(ctx, invitation) +} + +// DeleteInvitation deletes one unaccepted invitation to the specified Atlas organization. You can't delete an invitation that a user has accepted. +// +// See more: https://docs.atlas.mongodb.com/reference/api/organization-delete-invitation/ +func (s *OrganizationsServiceOp) DeleteInvitation(ctx context.Context, orgID, invitationID string) (*Response, error) { + if orgID == "" { + return nil, NewArgError("orgID", "must be set") + } + + if invitationID == "" { + return nil, NewArgError("invitationID", "must be set") + } + + basePath := fmt.Sprintf(invitationBasePath, orgID) + path := fmt.Sprintf("%s/%s", basePath, invitationID) + + req, err := s.Client.NewRequest(ctx, http.MethodDelete, path, nil) + if err != nil { + return nil, err + } + + resp, err := s.Client.Do(ctx, req, nil) + + return resp, err +} + +func (s *OrganizationsServiceOp) updateInvitation(ctx context.Context, invitation *Invitation) (*Invitation, *Response, error) { + path := fmt.Sprintf(invitationBasePath, invitation.OrgID) + + if invitation.ID != "" { + path = fmt.Sprintf("%s/%s", path, invitation.ID) + } + + req, err := s.Client.NewRequest(ctx, http.MethodPatch, path, invitation) + if err != nil { + return nil, nil, err + } + + root := new(Invitation) + resp, err := s.Client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root, resp, nil +} diff --git a/mongodbatlas/organization_invitations_test.go b/mongodbatlas/organization_invitations_test.go new file mode 100644 index 000000000..8dce88fc6 --- /dev/null +++ b/mongodbatlas/organization_invitations_test.go @@ -0,0 +1,299 @@ +// Copyright 2021 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mongodbatlas + +import ( + "fmt" + "net/http" + "testing" + + "github.com/go-test/deep" +) + +const invitationID = "1" + +func TestOrganizations_Invitations(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc(fmt.Sprintf("/api/atlas/v1.0/orgs/%s/invites", orgID), func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + _, _ = fmt.Fprint(w, `[ + { + "createdAt": "2021-02-18T21:05:40Z", + "expiresAt": "2021-03-20T21:05:40Z", + "id": "5a0a1e7e0f2912c554080adc", + "inviterUsername": "admin@example.com", + "orgId": "5df7a168f10fab3a149357fb", + "orgName": "jww-12-16", + "roles": [ + "ORG_OWNER" + ], + "username": "wyatt.smith@example.com"}, + {"createdAt": "2021-02-18T21:05:40Z", + "expiresAt": "2021-03-20T21:05:40Z", + "id": "5a0a1e7e0f2912c554080adc", + "inviterUsername": "admin@example.com", + "orgId": "5df7a168f10fab3a149357fb", + "orgName": "jww-12-16", + "roles": [ + "ORG_OWNER" + ], + "teamIds": ["2"], + "username": "wyatt.smith@example.com"}]`) + }) + + invitation, _, err := client.Organizations.Invitations(ctx, orgID, nil) + if err != nil { + t.Fatalf("Organizations.Invitations returned error: %v", err) + } + + expected := []*Invitation{ + { + ID: "5a0a1e7e0f2912c554080adc", + OrgID: "5df7a168f10fab3a149357fb", + OrgName: "jww-12-16", + CreatedAt: "2021-02-18T21:05:40Z", + ExpiresAt: "2021-03-20T21:05:40Z", + InviterUsername: "admin@example.com", + Username: "wyatt.smith@example.com", + Roles: []string{"ORG_OWNER"}, + }, + { + ID: "5a0a1e7e0f2912c554080adc", + OrgID: "5df7a168f10fab3a149357fb", + OrgName: "jww-12-16", + CreatedAt: "2021-02-18T21:05:40Z", + ExpiresAt: "2021-03-20T21:05:40Z", + InviterUsername: "admin@example.com", + Username: "wyatt.smith@example.com", + Roles: []string{"ORG_OWNER"}, + TeamIDs: []string{"2"}, + }, + } + + if diff := deep.Equal(invitation, expected); diff != nil { + t.Error(diff) + } +} + +func TestOrganizations_Invitation(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc(fmt.Sprintf("/api/atlas/v1.0/orgs/%s/invites/%s", orgID, invitationID), func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + _, _ = fmt.Fprint(w, `{ + "createdAt": "2021-02-18T21:05:40Z", + "expiresAt": "2021-03-20T21:05:40Z", + "id": "5a0a1e7e0f2912c554080adc", + "inviterUsername": "admin@example.com", + "orgId": "5df7a168f10fab3a149357fb", + "orgName": "jww-12-16", + "roles": [ + "ORG_OWNER" + ], + "username": "wyatt.smith@example.com" + }`) + }) + + invitation, _, err := client.Organizations.Invitation(ctx, orgID, invitationID) + if err != nil { + t.Fatalf("Organizations.Invitation returned error: %v", err) + } + + expected := &Invitation{ + ID: "5a0a1e7e0f2912c554080adc", + OrgID: "5df7a168f10fab3a149357fb", + OrgName: "jww-12-16", + CreatedAt: "2021-02-18T21:05:40Z", + ExpiresAt: "2021-03-20T21:05:40Z", + InviterUsername: "admin@example.com", + Username: "wyatt.smith@example.com", + Roles: []string{"ORG_OWNER"}, + } + + if diff := deep.Equal(invitation, expected); diff != nil { + t.Error(diff) + } +} + +func TestOrganizations_InviteUser(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc(fmt.Sprintf("/api/atlas/v1.0/orgs/%s/invites", orgID), func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodPost) + _, _ = fmt.Fprint(w, `{ + "createdAt": "2021-02-18T21:05:40Z", + "expiresAt": "2021-03-20T21:05:40Z", + "id": "5a0a1e7e0f2912c554080adc", + "inviterUsername": "admin@example.com", + "orgId": "5df7a168f10fab3a149357fb", + "orgName": "jww-12-16", + "roles": [ + "ORG_OWNER" + ], + "username": "wyatt.smith@example.com" + }`) + }) + + body := &Invitation{ + OrgID: orgID, + OrgName: "jww-12-16", + CreatedAt: "2021-02-18T21:05:40Z", + ExpiresAt: "2021-03-20T21:05:40Z", + InviterUsername: "admin@example.com", + Username: "wyatt.smith@example.com", + Roles: []string{"ORG_OWNER"}, + } + + invitation, _, err := client.Organizations.InviteUser(ctx, body) + if err != nil { + t.Fatalf("Organizations.InviteUser returned error: %v", err) + } + + expected := &Invitation{ + ID: "5a0a1e7e0f2912c554080adc", + OrgID: "5df7a168f10fab3a149357fb", + OrgName: "jww-12-16", + CreatedAt: "2021-02-18T21:05:40Z", + ExpiresAt: "2021-03-20T21:05:40Z", + InviterUsername: "admin@example.com", + Username: "wyatt.smith@example.com", + Roles: []string{"ORG_OWNER"}, + } + + if diff := deep.Equal(invitation, expected); diff != nil { + t.Error(diff) + } +} + +func TestOrganizations_UpdateInvitation(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc(fmt.Sprintf("/api/atlas/v1.0/orgs/%s/invites", orgID), func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodPatch) + _, _ = fmt.Fprint(w, `{ + "createdAt": "2021-02-18T21:05:40Z", + "expiresAt": "2021-03-20T21:05:40Z", + "id": "5a0a1e7e0f2912c554080adc", + "inviterUsername": "admin@example.com", + "orgId": "5df7a168f10fab3a149357fb", + "orgName": "jww-12-16", + "roles": [ + "ORG_OWNER" + ], + "username": "wyatt.smith@example.com" + }`) + }) + + body := &Invitation{ + OrgID: orgID, + OrgName: "jww-12-16", + CreatedAt: "2021-02-18T21:05:40Z", + ExpiresAt: "2021-03-20T21:05:40Z", + InviterUsername: "admin@example.com", + Username: "wyatt.smith@example.com", + Roles: []string{"ORG_OWNER"}, + } + + invitation, _, err := client.Organizations.UpdateInvitation(ctx, body) + if err != nil { + t.Fatalf("Organizations.UpdateInvitation returned error: %v", err) + } + + expected := &Invitation{ + ID: "5a0a1e7e0f2912c554080adc", + OrgID: "5df7a168f10fab3a149357fb", + OrgName: "jww-12-16", + CreatedAt: "2021-02-18T21:05:40Z", + ExpiresAt: "2021-03-20T21:05:40Z", + InviterUsername: "admin@example.com", + Username: "wyatt.smith@example.com", + Roles: []string{"ORG_OWNER"}, + } + + if diff := deep.Equal(invitation, expected); diff != nil { + t.Error(diff) + } +} + +func TestOrganizations_UpdateInvitationByID(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc(fmt.Sprintf("/api/atlas/v1.0/orgs/%s/invites/%s", orgID, invitationID), func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodPatch) + _, _ = fmt.Fprint(w, `{ + "createdAt": "2021-02-18T21:05:40Z", + "expiresAt": "2021-03-20T21:05:40Z", + "id": "5a0a1e7e0f2912c554080adc", + "inviterUsername": "admin@example.com", + "orgId": "5df7a168f10fab3a149357fb", + "orgName": "jww-12-16", + "roles": [ + "ORG_OWNER" + ], + "username": "wyatt.smith@example.com" + }`) + }) + + body := &Invitation{ + OrgID: orgID, + ID: invitationID, + OrgName: "jww-12-16", + CreatedAt: "2021-02-18T21:05:40Z", + ExpiresAt: "2021-03-20T21:05:40Z", + InviterUsername: "admin@example.com", + Username: "wyatt.smith@example.com", + Roles: []string{"ORG_OWNER"}, + } + + invitation, _, err := client.Organizations.UpdateInvitationByID(ctx, invitationID, body) + if err != nil { + t.Fatalf("Organizations.UpdateInvitationByID returned error: %v", err) + } + + expected := &Invitation{ + ID: "5a0a1e7e0f2912c554080adc", + OrgID: "5df7a168f10fab3a149357fb", + OrgName: "jww-12-16", + CreatedAt: "2021-02-18T21:05:40Z", + ExpiresAt: "2021-03-20T21:05:40Z", + InviterUsername: "admin@example.com", + Username: "wyatt.smith@example.com", + Roles: []string{"ORG_OWNER"}, + } + + if diff := deep.Equal(invitation, expected); diff != nil { + t.Error(diff) + } +} + +func TestOrganizations_DeleteInvitation(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc(fmt.Sprintf("/api/atlas/v1.0/orgs/%s/invites/%s", orgID, invitationID), func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodDelete) + }) + + _, err := client.Organizations.DeleteInvitation(ctx, orgID, invitationID) + if err != nil { + t.Fatalf("Organizations.DeleteInvitation returned error: %v", err) + } +} diff --git a/mongodbatlas/organizations.go b/mongodbatlas/organizations.go index d023b2484..4367eab1c 100644 --- a/mongodbatlas/organizations.go +++ b/mongodbatlas/organizations.go @@ -20,19 +20,23 @@ import ( "net/http" ) -const ( - orgsBasePath = "api/atlas/v1.0/orgs" -) +const orgsBasePath = "api/atlas/v1.0/orgs" // OrganizationsService provides access to the organization related functions in the Atlas API. // // See more: https://docs.atlas.mongodb.com/reference/api/organizations/ type OrganizationsService interface { List(context.Context, *OrganizationsListOptions) (*Organizations, *Response, error) + Invitations(context.Context, string, *InvitationOptions) ([]*Invitation, *Response, error) Get(context.Context, string) (*Organization, *Response, error) + Invitation(context.Context, string, string) (*Invitation, *Response, error) Projects(context.Context, string, *ListOptions) (*Projects, *Response, error) Users(context.Context, string, *ListOptions) (*AtlasUsersResponse, *Response, error) Delete(context.Context, string) (*Response, error) + InviteUser(context.Context, *Invitation) (*Invitation, *Response, error) + UpdateInvitation(context.Context, *Invitation) (*Invitation, *Response, error) + UpdateInvitationByID(context.Context, string, *Invitation) (*Invitation, *Response, error) + DeleteInvitation(context.Context, string, string) (*Response, error) } // OrganizationsServiceOp provides an implementation of the OrganizationsService interface. diff --git a/mongodbatlas/organizations_test.go b/mongodbatlas/organizations_test.go index c3653bcc4..5bc6fe7a0 100644 --- a/mongodbatlas/organizations_test.go +++ b/mongodbatlas/organizations_test.go @@ -22,6 +22,8 @@ import ( "github.com/go-test/deep" ) +const orgID = "5a0a1e7e0f2912c554080adc" + func TestOrganizationsServiceOp_List(t *testing.T) { t.Run("default", func(t *testing.T) { client, mux, teardown := setup() @@ -174,9 +176,7 @@ func TestOrganizationsServiceOp_Get(t *testing.T) { client, mux, teardown := setup() defer teardown() - ID := "5a0a1e7e0f2912c554080adc" - - mux.HandleFunc(fmt.Sprintf("/%s/%s", orgsBasePath, ID), func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc(fmt.Sprintf("/%s/%s", orgsBasePath, orgID), func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) _, _ = fmt.Fprint(w, `{ "id": "5a0a1e7e0f2912c554080adc", @@ -189,7 +189,7 @@ func TestOrganizationsServiceOp_Get(t *testing.T) { }`) }) - response, _, err := client.Organizations.Get(ctx, ID) + response, _, err := client.Organizations.Get(ctx, orgID) if err != nil { t.Fatalf("Organizations.Get returned error: %v", err) } @@ -214,13 +214,11 @@ func TestOrganizationsServiceOp_Projects(t *testing.T) { client, mux, teardown := setup() defer teardown() - ID := "5980cfdf0b6d97029d82f86e" - - mux.HandleFunc(fmt.Sprintf("/%s/%s/groups", orgsBasePath, ID), func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc(fmt.Sprintf("/%s/%s/groups", orgsBasePath, orgID), func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) _, _ = fmt.Fprint(w, `{ "links": [{ - "href": "https://cloud.mongodb.com/api/public/v1.0/orgs/5980cfdf0b6d97029d82f86e/groups", + "href": "https://cloud.mongodb.com/api/public/v1.0/orgs/5a0a1e7e0f2912c554080adc/groups", "rel": "self" }], "results": [{ @@ -230,13 +228,13 @@ func TestOrganizationsServiceOp_Projects(t *testing.T) { "rel": "self" }], "name": "012i3091203jioawjioej", - "orgId": "5980cfdf0b6d97029d82f86e" + "orgId": "5a0a1e7e0f2912c554080adc" }], "totalCount": 1 }`) }) - projects, _, err := client.Organizations.Projects(ctx, ID, nil) + projects, _, err := client.Organizations.Projects(ctx, orgID, nil) if err != nil { t.Fatalf("Organizations.GetProjects returned error: %v", err) } @@ -244,7 +242,7 @@ func TestOrganizationsServiceOp_Projects(t *testing.T) { expected := &Projects{ Links: []*Link{ { - Href: "https://cloud.mongodb.com/api/public/v1.0/orgs/5980cfdf0b6d97029d82f86e/groups", + Href: "https://cloud.mongodb.com/api/public/v1.0/orgs/5a0a1e7e0f2912c554080adc/groups", Rel: "self", }, }, @@ -258,7 +256,7 @@ func TestOrganizationsServiceOp_Projects(t *testing.T) { }, }, Name: "012i3091203jioawjioej", - OrgID: "5980cfdf0b6d97029d82f86e", + OrgID: "5a0a1e7e0f2912c554080adc", }, }, TotalCount: 1, @@ -273,9 +271,7 @@ func TestOrganizationsServiceOp_Users(t *testing.T) { client, mux, teardown := setup() defer teardown() - ID := "5980cfdf0b6d97029d82f86e" - - mux.HandleFunc(fmt.Sprintf("/%s/%s/users", orgsBasePath, ID), func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc(fmt.Sprintf("/%s/%s/users", orgsBasePath, orgID), func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) _, _ = fmt.Fprint(w, `{ "links": [ @@ -343,7 +339,7 @@ func TestOrganizationsServiceOp_Users(t *testing.T) { }`) }) - users, _, err := client.Organizations.Users(ctx, ID, nil) + users, _, err := client.Organizations.Users(ctx, orgID, nil) if err != nil { t.Fatalf("Organizations.Users returned error: %v", err) } @@ -402,8 +398,6 @@ func TestOrganizations_Delete(t *testing.T) { client, mux, teardown := setup() defer teardown() - orgID := "5a0a1e7e0f2912c554080adc" - mux.HandleFunc(fmt.Sprintf("/api/atlas/v1.0/orgs/%s", orgID), func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) })