Skip to content

Commit c626b72

Browse files
CLOUDP-309670: [Atlas CLI] - Bug when creating org invite (#3789)
1 parent c11134b commit c626b72

File tree

6 files changed

+243
-2
lines changed

6 files changed

+243
-2
lines changed

docs/command/atlas-organizations-invitations-update.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ Options
6363
- string
6464
- false
6565
- Path to an optional JSON configuration file that defines invitation settings. Note: Unsupported fields in the JSON file are ignored.
66+
67+
Mutually exclusive with --role.
6668
* - -h, --help
6769
-
6870
- false
@@ -77,9 +79,11 @@ Options
7779
- Output format. Valid values are json, json-path, go-template, or go-template-file. To see the full output, use the -o json option.
7880
* - --role
7981
- strings
80-
- true
82+
- false
8183
- User's roles for the associated organization. Valid values include ORG_OWNER, ORG_MEMBER, ORG_GROUP_CREATOR, ORG_BILLING_ADMIN, and ORG_READ_ONLY. Passing this flag replaces preexisting data.
8284

85+
Mutually exclusive with --file.
86+
8387
Inherited Options
8488
-----------------
8589

internal/cli/organizations/invitations/invite.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ func (opts *InviteOpts) newInvitation() (*atlasv2.OrganizationInvitationRequest,
8585
func InviteBuilder() *cobra.Command {
8686
opts := new(InviteOpts)
8787
opts.Template = createTemplate
88+
opts.fs = afero.NewOsFs() // Initialize the filesystem
8889
cmd := &cobra.Command{
8990
Use: "invite <email>",
9091
Short: "Invite the specified MongoDB user to your organization.",

internal/cli/organizations/invitations/invite_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@
1717
package invitations
1818

1919
import (
20+
"encoding/json"
2021
"testing"
2122

2223
"github.com/golang/mock/gomock"
2324
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/mocks"
25+
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/pointer"
26+
"github.com/spf13/afero"
2427
"github.com/stretchr/testify/require"
2528
"go.mongodb.org/atlas-sdk/v20250312001/admin"
2629
)
@@ -47,3 +50,43 @@ func TestCreate_Run(t *testing.T) {
4750
t.Fatalf("Run() unexpected error: %v", err)
4851
}
4952
}
53+
54+
func TestInvite_Run_WithFile(t *testing.T) {
55+
ctrl := gomock.NewController(t)
56+
mockStore := mocks.NewMockOrganizationInviter(ctrl)
57+
fs := afero.NewMemMapFs()
58+
59+
testFile := "invitation.json"
60+
_, _ = fs.Create(testFile)
61+
invitation := &admin.OrganizationInvitationRequest{
62+
Username: pointer.Get("[email protected]"),
63+
Roles: pointer.Get([]string{"ORG_READ_ONLY"}),
64+
TeamIds: pointer.Get([]string{"5f71e5255afec75a3d0f96dc"}),
65+
GroupRoleAssignments: pointer.Get([]admin.OrganizationInvitationGroupRoleAssignmentsRequest{
66+
{
67+
GroupId: pointer.Get("6c73999ae7966f00563911a4"),
68+
Roles: pointer.Get([]string{"GROUP_CLUSTER_MANAGER"}),
69+
},
70+
}),
71+
}
72+
invitationJSON, err := json.Marshal(invitation)
73+
require.NoError(t, err)
74+
_ = afero.WriteFile(fs, testFile, invitationJSON, 0600)
75+
76+
expectedResult := &admin.OrganizationInvitation{}
77+
78+
opts := &InviteOpts{
79+
store: mockStore,
80+
fs: fs,
81+
filename: testFile,
82+
}
83+
84+
mockStore.
85+
EXPECT().
86+
InviteUser(opts.ConfigOrgID(), invitation).Return(expectedResult, nil).
87+
Times(1)
88+
89+
if err := opts.Run(); err != nil {
90+
t.Fatalf("Run() unexpected error: %v", err)
91+
}
92+
}

internal/cli/organizations/invitations/update.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ func (opts *UpdateOpts) validate() error {
9292
// atlas organization(s) invitation(s) updates [invitationId] --role role [--orgId orgId] [--email email].
9393
func UpdateBuilder() *cobra.Command {
9494
opts := &UpdateOpts{}
95+
opts.fs = afero.NewOsFs()
9596
cmd := &cobra.Command{
9697
Use: "update [invitationId]",
9798
Aliases: []string{"updates"},
@@ -132,7 +133,8 @@ func UpdateBuilder() *cobra.Command {
132133
opts.AddOrgOptFlags(cmd)
133134
opts.AddOutputOptFlags(cmd)
134135

135-
_ = cmd.MarkFlagRequired(flag.Role)
136+
_ = cmd.MarkFlagFilename(flag.File)
137+
cmd.MarkFlagsMutuallyExclusive(flag.File, flag.Role)
136138

137139
return cmd
138140
}

internal/cli/organizations/invitations/update_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@
1717
package invitations
1818

1919
import (
20+
"encoding/json"
2021
"testing"
2122

2223
"github.com/golang/mock/gomock"
2324
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/cli"
2425
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/mocks"
26+
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/pointer"
27+
"github.com/spf13/afero"
2528
"github.com/stretchr/testify/require"
2629
"go.mongodb.org/atlas-sdk/v20250312001/admin"
2730
)
@@ -51,3 +54,44 @@ func TestUpdate_Run(t *testing.T) {
5154
t.Fatalf("Run() unexpected error: %v", err)
5255
}
5356
}
57+
58+
func TestUpdate_Run_WithFile(t *testing.T) {
59+
ctrl := gomock.NewController(t)
60+
mockStore := mocks.NewMockOrganizationInvitationUpdater(ctrl)
61+
fs := afero.NewMemMapFs()
62+
63+
testFile := "update_invitation.json"
64+
invitationID := "6d39e6f9a16946a1abc390d4"
65+
_, _ = fs.Create(testFile)
66+
invitation := &admin.OrganizationInvitationRequest{
67+
Roles: pointer.Get([]string{"ORG_MEMBER"}),
68+
GroupRoleAssignments: pointer.Get([]admin.OrganizationInvitationGroupRoleAssignmentsRequest{
69+
{
70+
GroupId: pointer.Get("6c73999ae7966f00563911a4"),
71+
Roles: pointer.Get([]string{"GROUP_CLUSTER_MANAGER"}),
72+
},
73+
}),
74+
}
75+
invitationJSON, err := json.Marshal(invitation)
76+
require.NoError(t, err)
77+
_ = afero.WriteFile(fs, testFile, invitationJSON, 0600)
78+
79+
expectedResult := &admin.OrganizationInvitation{}
80+
81+
opts := &UpdateOpts{
82+
store: mockStore,
83+
fs: fs,
84+
filename: testFile,
85+
invitationID: invitationID,
86+
OrgOpts: cli.OrgOpts{OrgID: "1"},
87+
}
88+
89+
mockStore.
90+
EXPECT().
91+
UpdateOrganizationInvitation(opts.ConfigOrgID(), invitationID, invitation).Return(expectedResult, nil).
92+
Times(1)
93+
94+
if err := opts.Run(); err != nil {
95+
t.Fatalf("Run() unexpected error: %v", err)
96+
}
97+
}

test/e2e/atlas_org_invitations_test.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"os/exec"
2323
"testing"
2424

25+
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/pointer"
2526
"github.com/stretchr/testify/assert"
2627
"github.com/stretchr/testify/require"
2728
"go.mongodb.org/atlas-sdk/v20250312001/admin"
@@ -36,6 +37,7 @@ func TestAtlasOrgInvitations(t *testing.T) {
3637

3738
emailOrg := fmt.Sprintf("test-%[email protected]", n)
3839
var orgInvitationID string
40+
var orgInvitationIDFile string // For the file-based invite test
3941

4042
g.Run("Invite", func(t *testing.T) { //nolint:thelper // g.Run replaces t.Run
4143
cmd := exec.Command(cliPath,
@@ -54,10 +56,73 @@ func TestAtlasOrgInvitations(t *testing.T) {
5456
var invitation admin.OrganizationInvitation
5557
require.NoError(t, json.Unmarshal(resp, &invitation))
5658
a.Equal(emailOrg, invitation.GetUsername())
59+
a.Equal([]string{"ORG_MEMBER"}, invitation.GetRoles())
5760
require.NotEmpty(t, invitation.GetId())
5861
orgInvitationID = invitation.GetId()
5962
})
6063

64+
g.Run("Invite with File", func(t *testing.T) { //nolint:thelper // g.Run replaces t.Run
65+
a := assert.New(t)
66+
// Create a unique email for this test
67+
nFile := g.memoryRand("randFile", 1000)
68+
emailOrgFile := fmt.Sprintf("test-file-%[email protected]", nFile)
69+
70+
inviteData := admin.OrganizationInvitationRequest{
71+
Username: pointer.Get(emailOrgFile),
72+
Roles: pointer.Get([]string{"ORG_READ_ONLY"}),
73+
}
74+
inviteFilename := fmt.Sprintf("%s/update-%s.json", t.TempDir(), nFile)
75+
createJSONFile(t, inviteData, inviteFilename)
76+
77+
cmd := exec.Command(cliPath,
78+
orgEntity,
79+
invitationsEntity,
80+
"invite",
81+
"--file", inviteFilename,
82+
"-o=json")
83+
cmd.Env = os.Environ()
84+
resp, err := RunAndGetStdOut(cmd)
85+
require.NoError(t, err, string(resp))
86+
87+
var invitation admin.OrganizationInvitation
88+
require.NoError(t, json.Unmarshal(resp, &invitation))
89+
a.Equal(emailOrgFile, invitation.GetUsername())
90+
a.Equal(inviteData.GetRoles(), invitation.GetRoles())
91+
require.NotEmpty(t, invitation.GetId())
92+
orgInvitationIDFile = invitation.GetId() // Save ID for cleanup
93+
})
94+
95+
g.Run("Invite with File", func(t *testing.T) { //nolint:thelper // g.Run replaces t.Run
96+
a := assert.New(t)
97+
// Create a unique email for this test
98+
nFile := g.memoryRand("randFile", 1000)
99+
emailOrgFile := fmt.Sprintf("test-file-%[email protected]", nFile)
100+
101+
inviteData := admin.OrganizationInvitationRequest{
102+
Username: pointer.Get(emailOrgFile),
103+
Roles: pointer.Get([]string{"ORG_READ_ONLY"}),
104+
}
105+
inviteFilename := fmt.Sprintf("%s/update-%s.json", t.TempDir(), nFile)
106+
createJSONFile(t, inviteData, inviteFilename)
107+
108+
cmd := exec.Command(cliPath,
109+
orgEntity,
110+
invitationsEntity,
111+
"invite",
112+
"--file", inviteFilename,
113+
"-o=json")
114+
cmd.Env = os.Environ()
115+
resp, err := RunAndGetStdOut(cmd)
116+
require.NoError(t, err, string(resp))
117+
118+
var invitation admin.OrganizationInvitation
119+
require.NoError(t, json.Unmarshal(resp, &invitation))
120+
a.Equal(emailOrgFile, invitation.GetUsername())
121+
a.Equal(inviteData.GetRoles(), invitation.GetRoles())
122+
require.NotEmpty(t, invitation.GetId())
123+
orgInvitationIDFile = invitation.GetId() // Save ID for cleanup
124+
})
125+
61126
g.Run("List", func(t *testing.T) { //nolint:thelper // g.Run replaces t.Run
62127
cmd := exec.Command(cliPath,
63128
orgEntity,
@@ -136,6 +201,72 @@ func TestAtlasOrgInvitations(t *testing.T) {
136201
a.ElementsMatch([]string{roleNameOrg}, invitation.GetRoles())
137202
})
138203

204+
const OrgGroupCreator = "ORG_GROUP_CREATOR"
205+
206+
g.Run("Update with File", func(t *testing.T) { //nolint:thelper // g.Run replaces t.Run
207+
require.NotEmpty(t, orgInvitationID, "orgInvitationID must be set by Invite test")
208+
a := assert.New(t)
209+
210+
nFile := g.memoryRand("randFile", 1000)
211+
212+
// Define the update data, including GroupRoleAssignments if desired
213+
updateRole := OrgGroupCreator
214+
updateData := admin.OrganizationInvitationRequest{
215+
Roles: pointer.Get([]string{updateRole}),
216+
}
217+
updateFilename := fmt.Sprintf("%s/update-%s.json", t.TempDir(), nFile)
218+
createJSONFile(t, updateData, updateFilename)
219+
220+
cmd := exec.Command(cliPath,
221+
orgEntity,
222+
invitationsEntity,
223+
"update",
224+
orgInvitationID, // Use ID from the original Invite test
225+
"--file", updateFilename,
226+
"-o=json")
227+
cmd.Env = os.Environ()
228+
resp, err := RunAndGetStdOut(cmd)
229+
require.NoError(t, err, string(resp))
230+
231+
var invitation admin.OrganizationInvitation
232+
require.NoError(t, json.Unmarshal(resp, &invitation))
233+
a.Equal(orgInvitationID, invitation.GetId())
234+
a.ElementsMatch(updateData.GetRoles(), invitation.GetRoles()) // Check if roles were updated
235+
// Add assertions for GroupRoleAssignments if included in updateData
236+
})
237+
238+
g.Run("Update with File", func(t *testing.T) { //nolint:thelper // g.Run replaces t.Run
239+
require.NotEmpty(t, orgInvitationID, "orgInvitationID must be set by Invite test")
240+
a := assert.New(t)
241+
242+
nFile := g.memoryRand("randFile", 1000)
243+
244+
// Define the update data, including GroupRoleAssignments if desired
245+
updateRole := OrgGroupCreator
246+
updateData := admin.OrganizationInvitationRequest{
247+
Roles: pointer.Get([]string{updateRole}),
248+
}
249+
updateFilename := fmt.Sprintf("%s/update-%s.json", t.TempDir(), nFile)
250+
createJSONFile(t, updateData, updateFilename)
251+
252+
cmd := exec.Command(cliPath,
253+
orgEntity,
254+
invitationsEntity,
255+
"update",
256+
orgInvitationID, // Use ID from the original Invite test
257+
"--file", updateFilename,
258+
"-o=json")
259+
cmd.Env = os.Environ()
260+
resp, err := RunAndGetStdOut(cmd)
261+
require.NoError(t, err, string(resp))
262+
263+
var invitation admin.OrganizationInvitation
264+
require.NoError(t, json.Unmarshal(resp, &invitation))
265+
a.Equal(orgInvitationID, invitation.GetId())
266+
a.ElementsMatch(updateData.GetRoles(), invitation.GetRoles()) // Check if roles were updated
267+
// Add assertions for GroupRoleAssignments if included in updateData
268+
})
269+
139270
g.Run("Delete", func(t *testing.T) { //nolint:thelper // g.Run replaces t.Run
140271
cmd := exec.Command(cliPath,
141272
orgEntity,
@@ -150,4 +281,20 @@ func TestAtlasOrgInvitations(t *testing.T) {
150281
expected := fmt.Sprintf("Invitation '%s' deleted\n", orgInvitationID)
151282
a.Equal(expected, string(resp))
152283
})
284+
285+
g.Run("Delete Invitation from File Test", func(t *testing.T) { //nolint:thelper // g.Run replaces t.Run
286+
require.NotEmpty(t, orgInvitationIDFile, "orgInvitationIDFile must be set by Invite with File test")
287+
cmd := exec.Command(cliPath,
288+
orgEntity,
289+
invitationsEntity,
290+
"delete",
291+
orgInvitationIDFile,
292+
"--force")
293+
cmd.Env = os.Environ()
294+
resp, err := RunAndGetStdOut(cmd)
295+
a := assert.New(t)
296+
require.NoError(t, err, string(resp))
297+
expected := fmt.Sprintf("Invitation '%s' deleted\n", orgInvitationIDFile)
298+
a.Equal(expected, string(resp))
299+
})
153300
}

0 commit comments

Comments
 (0)