Skip to content

Commit b576f3d

Browse files
abshierjoelwdoconnellhoorayimhelpingeatondustin1
authored
feat(ecommerce): Multi-Org Invitation within Same Account (#5056)
* ✨ Handle auto-add to organization for invitations Co-authored-by: Bill O'Connell <91283923+wdoconnell@users.noreply.github.com> Co-authored-by: Bucky Schwarz <hoorayimhelping@users.noreply.github.com> Co-authored-by: Dustin Eaton <deaton@influxdata.com> * 🚨 Remove unused imports Co-authored-by: Bill O'Connell <91283923+wdoconnell@users.noreply.github.com> Co-authored-by: Bucky Schwarz <hoorayimhelping@users.noreply.github.com> Co-authored-by: Dustin Eaton <deaton@influxdata.com> Co-authored-by: Bill O'Connell <91283923+wdoconnell@users.noreply.github.com> Co-authored-by: Bucky Schwarz <hoorayimhelping@users.noreply.github.com> Co-authored-by: Dustin Eaton <deaton@influxdata.com>
1 parent 6f2b2d0 commit b576f3d

File tree

3 files changed

+166
-7
lines changed

3 files changed

+166
-7
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
"cy": "CYPRESS_dexUrl=https://$INGRESS_HOST:$PORT_HTTPS CYPRESS_baseUrl=http://localhost:9999 cypress open",
5353
"cy:dev": "source ../monitor-ci/.env && CYPRESS_dexUrl=CLOUD CYPRESS_baseUrl=https://$INGRESS_HOST:$PORT_HTTPS cypress open --config testFiles='{cloud,shared}/**/*.*'",
5454
"cy:dev-oss": "source ../monitor-ci/.env && CYPRESS_dexUrl=OSS CYPRESS_baseUrl=https://$INGRESS_HOST:$PORT_HTTPS cypress open --config testFiles='{oss,shared}/**/*.*'",
55-
"generate": "export SHA=3ef04defe045c8333f458f0cc2959057183d217f && export REMOTE=https://raw.githubusercontent.com/influxdata/openapi/${SHA}/ && yarn generate-meta",
55+
"generate": "export SHA=08b0a56cb105ab58f22820ecdd8f93cb4dd8203d && export REMOTE=https://raw.githubusercontent.com/influxdata/openapi/${SHA}/ && yarn generate-meta",
5656
"generate-local": "export REMOTE=../openapi/ && yarn generate-meta",
5757
"generate-meta": "if [ -z \"${CLOUD_URL}\" ]; then yarn generate-meta-oss; else yarn generate-meta-cloud; fi",
5858
"generate-meta-oss": "yarn oss-api && yarn notebooks && yarn unity && yarn annotations-oss && yarn pinned && yarn mapsd-oss && yarn uiproxyd-oss && yarn cloudPriv && yarn fluxdocs && yarn subscriptions-oss",
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import React from 'react'
2+
import {fireEvent, waitFor} from '@testing-library/react'
3+
import {mocked} from 'ts-jest/utils'
4+
import {act} from 'react-dom/test-utils'
5+
6+
import {renderWithReduxAndRouter} from 'src/mockState'
7+
import {UsersProvider} from '../context/users'
8+
import UsersPage from './UsersPage'
9+
10+
jest.mock(
11+
'src/client/unityRoutes',
12+
() => ({
13+
getOrgsUsers: jest.fn(() =>
14+
Promise.resolve({
15+
status: 200,
16+
headers: {},
17+
data: [
18+
{
19+
id: '1',
20+
firstName: 'Test',
21+
lastName: 'User',
22+
email: 'initial+user@example.com',
23+
role: 'owner',
24+
},
25+
],
26+
})
27+
),
28+
getOrgsInvites: jest.fn(() =>
29+
Promise.resolve({
30+
status: 200,
31+
headers: {},
32+
data: [
33+
{
34+
id: 789,
35+
email: 'initial+invite@example.com',
36+
role: 'owner',
37+
expiresAt: '2022-07-16T19:01:24.841104Z',
38+
},
39+
],
40+
})
41+
),
42+
postOrgsInvite: jest.fn(),
43+
}),
44+
{virtual: true}
45+
)
46+
47+
import {postOrgsInvite} from 'src/client/unityRoutes'
48+
49+
const setup = () => {
50+
return renderWithReduxAndRouter(
51+
<>
52+
<UsersProvider>
53+
<UsersPage />
54+
</UsersProvider>
55+
</>
56+
)
57+
}
58+
59+
describe('Inviting Users to an Organization', () => {
60+
it('renders existing users', async () => {
61+
const {getByTestId} = setup()
62+
63+
await waitFor(() => {
64+
const userListItem = getByTestId(
65+
'user-list-item initial+user@example.com'
66+
)
67+
68+
expect(userListItem).toHaveTextContent('Active')
69+
})
70+
})
71+
72+
it('renders existing invites', async () => {
73+
const {getByTestId} = setup()
74+
75+
await waitFor(() => {
76+
const inviteListItem = getByTestId(
77+
'invite-list-item initial+invite@example.com'
78+
)
79+
expect(inviteListItem).toHaveTextContent('Invite expiration 7/16/2022')
80+
})
81+
})
82+
83+
it('adds a user who belongs to an account to the organization', async () => {
84+
const {getByTestId} = setup()
85+
86+
const newUserEmail = 'new+user@example.com'
87+
88+
mocked(postOrgsInvite).mockImplementation(() =>
89+
Promise.resolve({
90+
status: 201,
91+
headers: {} as Headers,
92+
data: {
93+
id: 456,
94+
email: newUserEmail,
95+
role: 'owner',
96+
expiresAt: `2022-07-16T19:01:24.841104Z`,
97+
},
98+
})
99+
)
100+
101+
await waitFor(() => {
102+
const emailInput = getByTestId('email--input')
103+
fireEvent.change(emailInput, {target: {value: newUserEmail}})
104+
})
105+
106+
await act(async () => {
107+
const inviteButton = getByTestId('user-list-invite--button')
108+
await fireEvent.click(inviteButton)
109+
})
110+
111+
await waitFor(() => {
112+
const userListItem = getByTestId(`invite-list-item ${newUserEmail}`)
113+
expect(userListItem).toHaveTextContent('Invite expiration 7/16/2022')
114+
})
115+
})
116+
117+
it('adds a user to the organization, if they already belong to the account', async () => {
118+
const {getByTestId} = setup()
119+
120+
const existingEmail = 'existing+user@example.com'
121+
122+
mocked(postOrgsInvite).mockImplementation(() =>
123+
Promise.resolve({
124+
status: 200,
125+
headers: {} as Headers,
126+
data: {
127+
id: '101',
128+
firstName: 'Test',
129+
lastName: 'User Added to Org',
130+
email: existingEmail,
131+
role: 'owner',
132+
},
133+
})
134+
)
135+
136+
await waitFor(() => {
137+
const emailInput = getByTestId('email--input')
138+
fireEvent.change(emailInput, {target: {value: existingEmail}})
139+
})
140+
141+
await act(async () => {
142+
const inviteButton = getByTestId('user-list-invite--button')
143+
await fireEvent.click(inviteButton)
144+
})
145+
146+
await waitFor(() => {
147+
const userListItem = getByTestId(`user-list-item ${existingEmail}`)
148+
expect(userListItem).toHaveTextContent('Active')
149+
})
150+
})
151+
})

src/users/context/users.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
invitationWithdrawnFailed,
2424
removeUserFailed,
2525
removeUserSuccessful,
26+
memberAddSuccess,
2627
} from 'src/shared/copy/notifications'
2728

2829
// Constants
@@ -130,13 +131,20 @@ export const UsersProvider: FC<Props> = React.memo(({children}) => {
130131
try {
131132
const resp = await postOrgsInvite({orgId, data: draftInvite})
132133

133-
if (resp.status !== 201) {
134-
throw new Error(resp.data.message)
134+
switch (resp.status) {
135+
case 201:
136+
setInvites(prevInvites => [resp.data, ...prevInvites])
137+
dispatch(notify(inviteSent()))
138+
setDraftInvite(draft)
139+
break
140+
case 200:
141+
setUsers(prevUsers => [resp.data, ...prevUsers])
142+
dispatch(notify(memberAddSuccess(resp.data.email)))
143+
setDraftInvite(draft)
144+
break
145+
default:
146+
throw new Error(resp.data.message)
135147
}
136-
137-
setInvites(prevInvites => [resp.data, ...prevInvites])
138-
dispatch(notify(inviteSent()))
139-
setDraftInvite(draft)
140148
} catch (error) {
141149
dispatch(notify(inviteFailed()))
142150
console.error(error)

0 commit comments

Comments
 (0)