Resend invite (#4274)
* Set up resending invite * Resend invitation * Set up resending invite in frontend * Destroy invitation along with organization * Fix lint issues * Remove accepted flag * Automatically add pending invitation in OrganizationStore
This commit is contained in:
parent
6aa50e0eb5
commit
0354e19556
|
@ -8,6 +8,7 @@
|
|||
"shopify.vscode-shadowenv",
|
||||
"editorconfig.editorconfig",
|
||||
"dracula-theme.theme-dracula",
|
||||
"esbenp.prettier-vscode"
|
||||
"esbenp.prettier-vscode",
|
||||
"Orta.vscode-jest",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -24,5 +24,6 @@
|
|||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"files.exclude": {
|
||||
"**/node_modules": true
|
||||
}
|
||||
},
|
||||
"jest.jestCommandLine": "yarn test"
|
||||
}
|
||||
|
|
|
@ -1,26 +1,28 @@
|
|||
import OrganizationStore from '../stores/OrganizationStore';
|
||||
import { Role } from '../graphql/types';
|
||||
import { UserBasicInfo } from '@/models/UserBasicInfo';
|
||||
import { OrganizationDetail } from '@/models';
|
||||
|
||||
jest.mock('@apollo/client');
|
||||
|
||||
describe('OrganizationStore', () => {
|
||||
const userOne = {
|
||||
const userOne: UserBasicInfo = {
|
||||
id: 'user-1',
|
||||
email: 'user1@email.com',
|
||||
name: 'User One',
|
||||
account: { name: 'user-one' },
|
||||
accountName: 'user-one',
|
||||
avatarUrl: '',
|
||||
};
|
||||
const userTwo = {
|
||||
const userTwo: UserBasicInfo = {
|
||||
id: 'user-2',
|
||||
email: 'user2@email.com',
|
||||
name: 'User Two',
|
||||
account: { name: 'user-two' },
|
||||
accountName: 'user-two',
|
||||
avatarUrl: '',
|
||||
};
|
||||
const admin = {
|
||||
const admin: UserBasicInfo = {
|
||||
id: 'admin',
|
||||
email: 'admin@email.com',
|
||||
name: 'Admin',
|
||||
account: { name: 'admin' },
|
||||
accountName: 'admin',
|
||||
avatarUrl: '',
|
||||
};
|
||||
|
||||
const client = {
|
||||
|
@ -35,32 +37,32 @@ describe('OrganizationStore', () => {
|
|||
it('loads organization', async () => {
|
||||
// Given
|
||||
const organization = {
|
||||
__typename: 'Organization' as 'Organization' | undefined,
|
||||
id: 'organization-id',
|
||||
users: [userOne, userTwo],
|
||||
admins: [admin],
|
||||
pendingInvitations: [],
|
||||
};
|
||||
const organizationStore = new OrganizationStore(client as any);
|
||||
const expectedUserOne = {
|
||||
role: Role.User,
|
||||
id: userOne.id,
|
||||
email: userOne.email,
|
||||
name: userOne.account.name,
|
||||
avatarUrl: undefined,
|
||||
name: userOne.accountName,
|
||||
avatarUrl: '',
|
||||
};
|
||||
const expectedUserTwo = {
|
||||
role: Role.User,
|
||||
id: userTwo.id,
|
||||
email: userTwo.email,
|
||||
name: userTwo.account.name,
|
||||
avatarUrl: undefined,
|
||||
name: userTwo.accountName,
|
||||
avatarUrl: '',
|
||||
};
|
||||
const expectedAdmin = {
|
||||
role: Role.Admin,
|
||||
id: admin.id,
|
||||
email: admin.email,
|
||||
name: admin.account.name,
|
||||
avatarUrl: undefined,
|
||||
name: admin.accountName,
|
||||
avatarUrl: '',
|
||||
};
|
||||
|
||||
// When
|
||||
|
@ -86,6 +88,7 @@ describe('OrganizationStore', () => {
|
|||
id: 'organization-id',
|
||||
users: [userOne, userTwo],
|
||||
admins: [admin],
|
||||
pendingInvitations: [],
|
||||
};
|
||||
const organizationStore = new OrganizationStore(client as any);
|
||||
organizationStore.organization = organization;
|
||||
|
@ -93,22 +96,22 @@ describe('OrganizationStore', () => {
|
|||
role: Role.User,
|
||||
id: userOne.id,
|
||||
email: userOne.email,
|
||||
name: userOne.account.name,
|
||||
avatarUrl: undefined,
|
||||
name: userOne.accountName,
|
||||
avatarUrl: '',
|
||||
};
|
||||
const expectedUserTwo = {
|
||||
role: Role.User,
|
||||
id: userTwo.id,
|
||||
email: userTwo.email,
|
||||
name: userTwo.account.name,
|
||||
avatarUrl: undefined,
|
||||
name: userTwo.accountName,
|
||||
avatarUrl: '',
|
||||
};
|
||||
const expectedUserThree = {
|
||||
role: Role.User,
|
||||
id: admin.id,
|
||||
email: admin.email,
|
||||
name: admin.account.name,
|
||||
avatarUrl: undefined,
|
||||
name: admin.accountName,
|
||||
avatarUrl: '',
|
||||
};
|
||||
|
||||
// When
|
||||
|
@ -126,26 +129,26 @@ describe('OrganizationStore', () => {
|
|||
it('removes a user from the organization', async () => {
|
||||
// Given
|
||||
const organization = {
|
||||
__typename: 'Organization' as 'Organization' | undefined,
|
||||
id: 'organization-id',
|
||||
users: [userOne, userTwo],
|
||||
admins: [admin],
|
||||
};
|
||||
pendingInvitations: [],
|
||||
} as OrganizationDetail;
|
||||
const organizationStore = new OrganizationStore(client as any);
|
||||
organizationStore.organization = organization;
|
||||
const expectedUserOne = {
|
||||
role: Role.User,
|
||||
id: userOne.id,
|
||||
email: userOne.email,
|
||||
name: userOne.account.name,
|
||||
avatarUrl: undefined,
|
||||
name: userOne.accountName,
|
||||
avatarUrl: '',
|
||||
};
|
||||
const expectedAdmin = {
|
||||
role: Role.Admin,
|
||||
id: admin.id,
|
||||
email: admin.email,
|
||||
name: admin.account.name,
|
||||
avatarUrl: undefined,
|
||||
name: admin.accountName,
|
||||
avatarUrl: '',
|
||||
};
|
||||
|
||||
// When
|
||||
|
|
|
@ -31,10 +31,14 @@ const AcceptInvitationPage = observer(() => {
|
|||
primaryAction={{
|
||||
content: 'Accept the invitation',
|
||||
onAction: async () => {
|
||||
console.log('Accept me!');
|
||||
// TODO: Handle error (e.g. when already signed in but with a different account than the invitation was meant for)
|
||||
const slug =
|
||||
await acceptInvitationPageStore.acceptInvitation(
|
||||
token ?? '',
|
||||
);
|
||||
console.log('ola');
|
||||
console.log(slug);
|
||||
navigate(`/${slug}`);
|
||||
},
|
||||
}}
|
||||
|
|
|
@ -125,7 +125,7 @@ const OrganizationPage = observer(() => {
|
|||
.includes(userStore.me.id)) ??
|
||||
false;
|
||||
const [organizationPageStore] = useState(
|
||||
() => new OrganizationPageStore(),
|
||||
() => new OrganizationPageStore(organizationStore),
|
||||
);
|
||||
return (
|
||||
<Page
|
||||
|
@ -180,6 +180,42 @@ const OrganizationPage = observer(() => {
|
|||
}}
|
||||
/>
|
||||
</Card>
|
||||
{organizationPageStore.isPendingInvitationsVisible && (
|
||||
<Card title="Pending invitations">
|
||||
<ResourceList
|
||||
resourceName={{
|
||||
singular: 'pending invitation',
|
||||
plural: 'pending invitations',
|
||||
}}
|
||||
items={
|
||||
organizationStore.organization?.pendingInvitations ?? []
|
||||
}
|
||||
renderItem={({ inviteeEmail, id }) => {
|
||||
return (
|
||||
<div style={{ padding: '10px 100px 10px 20px' }}>
|
||||
<Stack alignment={'center'}>
|
||||
<Avatar customer size="medium" />
|
||||
<Stack.Item fill={true}>
|
||||
<TextStyle variation="strong">
|
||||
{inviteeEmail}
|
||||
</TextStyle>
|
||||
</Stack.Item>
|
||||
{isAdmin && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
organizationStore.resendInvite(id);
|
||||
}}
|
||||
>
|
||||
Resend invite
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,13 +1,24 @@
|
|||
import OrganizationStore from '@/stores/OrganizationStore';
|
||||
import { makeAutoObservable } from 'mobx';
|
||||
|
||||
class OrganizationPageStore {
|
||||
inviteeEmail = '';
|
||||
isInvitePopoverActive = false;
|
||||
|
||||
constructor() {
|
||||
private organizationStore: OrganizationStore;
|
||||
|
||||
constructor(organizationStore: OrganizationStore) {
|
||||
this.organizationStore = organizationStore;
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
get isPendingInvitationsVisible() {
|
||||
return (
|
||||
(this.organizationStore.organization?.pendingInvitations
|
||||
.length ?? 0) > 0
|
||||
);
|
||||
}
|
||||
|
||||
invitePopoverClosed() {
|
||||
this.isInvitePopoverActive = false;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import { OrganizationDetail } from '@/models';
|
||||
import OrganizationStore from '@/stores/OrganizationStore';
|
||||
import OrganizationPageStore from '../OrganizationPageStore';
|
||||
|
||||
jest.mock('@/stores/OrganizationStore');
|
||||
|
||||
describe('OrganizationPageStore', () => {
|
||||
const organizationStore = {} as OrganizationStore;
|
||||
|
||||
beforeEach(() => {
|
||||
organizationStore.organization = {
|
||||
id: 'organization',
|
||||
pendingInvitations: [],
|
||||
users: [],
|
||||
admins: [],
|
||||
} as OrganizationDetail;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('sets isPendingInvitationsVisible to false when there are pending invitations', () => {
|
||||
// Given
|
||||
organizationStore.organization!.pendingInvitations = [
|
||||
{ id: 'one', inviteeEmail: 'test@mail.com' },
|
||||
];
|
||||
|
||||
// When
|
||||
const organizationPageStore = new OrganizationPageStore(
|
||||
organizationStore,
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(organizationPageStore.isPendingInvitationsVisible).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('sets isPendingInvitationsVisible to true when there are not pending invitations', () => {
|
||||
// Given
|
||||
organizationStore.organization!.pendingInvitations = [];
|
||||
|
||||
// When
|
||||
const organizationPageStore = new OrganizationPageStore(
|
||||
organizationStore,
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(organizationPageStore.isPendingInvitationsVisible).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
import ProjectStore from '@/stores/ProjectStore';
|
||||
import RemoteCachePageStore from '../RemoteCachePageStore';
|
||||
import { Account, S3Bucket } from '@/models';
|
||||
import { S3Bucket } from '@/models';
|
||||
import { S3BucketInfoFragment } from '@/graphql/types';
|
||||
|
||||
jest.mock('@apollo/client');
|
||||
|
@ -29,7 +29,7 @@ describe('RemoteCachePageStore', () => {
|
|||
};
|
||||
});
|
||||
|
||||
it('keeps apply changes button disabled when not all fields are filled', async () => {
|
||||
it('keeps apply changes button disabled when not all fields are filled', () => {
|
||||
// Given
|
||||
const remoteCachePageStore = new RemoteCachePageStore(
|
||||
client,
|
||||
|
@ -46,7 +46,7 @@ describe('RemoteCachePageStore', () => {
|
|||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('marks apply changes button enabled when all fields are filled', async () => {
|
||||
it('marks apply changes button enabled when all fields are filled', () => {
|
||||
// Given
|
||||
const remoteCachePageStore = new RemoteCachePageStore(
|
||||
client,
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
query Invitation($token: String!) {
|
||||
invitation(token: $token) {
|
||||
organization {
|
||||
name
|
||||
}
|
||||
inviteeEmail
|
||||
inviter {
|
||||
...UserBasicInfo
|
||||
}
|
||||
...PendingInvitation
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,5 @@
|
|||
mutation InviteUser($input: InviteUserInput!) {
|
||||
inviteUser(input: $input) {
|
||||
inviteeEmail
|
||||
inviter {
|
||||
...UserBasicInfo
|
||||
}
|
||||
organization {
|
||||
account {
|
||||
name
|
||||
}
|
||||
}
|
||||
...PendingInvitation
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,5 +7,8 @@ query Organization($name: String!) {
|
|||
admins {
|
||||
...UserBasicInfo
|
||||
}
|
||||
invitations {
|
||||
...PendingInvitation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
fragment PendingInvitation on Invitation {
|
||||
inviteeEmail
|
||||
id
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
mutation ResendInvite($input: ResendInviteInput!) {
|
||||
resendInvite(input: $input) {
|
||||
inviteeEmail
|
||||
}
|
||||
}
|
|
@ -69,6 +69,7 @@ export type CreateS3BucketInput = {
|
|||
|
||||
export type Invitation = {
|
||||
__typename?: 'Invitation';
|
||||
id: Scalars['ID'];
|
||||
inviteeEmail: Scalars['ID'];
|
||||
inviter: User;
|
||||
organization: Organization;
|
||||
|
@ -99,6 +100,8 @@ export type Mutation = {
|
|||
inviteUser: Invitation;
|
||||
/** Remove user from a given organization */
|
||||
removeUser: User;
|
||||
/** Resend invite for a user to a given organization */
|
||||
resendInvite: Invitation;
|
||||
/** Update S3 bucket */
|
||||
updateS3Bucket: S3Bucket;
|
||||
};
|
||||
|
@ -139,6 +142,11 @@ export type MutationRemoveUserArgs = {
|
|||
};
|
||||
|
||||
|
||||
export type MutationResendInviteArgs = {
|
||||
input: ResendInviteInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateS3BucketArgs = {
|
||||
input: UpdateS3BucketInput;
|
||||
};
|
||||
|
@ -148,6 +156,7 @@ export type Organization = {
|
|||
account: Account;
|
||||
admins: Array<User>;
|
||||
id: Scalars['ID'];
|
||||
invitations: Array<Invitation>;
|
||||
name: Scalars['String'];
|
||||
users: Array<User>;
|
||||
};
|
||||
|
@ -214,6 +223,13 @@ export type RemoveUserInput = {
|
|||
userId: Scalars['ID'];
|
||||
};
|
||||
|
||||
/** Autogenerated input type of ResendInvite */
|
||||
export type ResendInviteInput = {
|
||||
/** A unique identifier for the client performing the mutation. */
|
||||
clientMutationId?: InputMaybe<Scalars['String']>;
|
||||
invitationId: Scalars['String'];
|
||||
};
|
||||
|
||||
export enum Role {
|
||||
Admin = 'admin',
|
||||
User = 'user'
|
||||
|
@ -291,14 +307,14 @@ export type InvitationQueryVariables = Exact<{
|
|||
}>;
|
||||
|
||||
|
||||
export type InvitationQuery = { __typename?: 'Query', invitation: { __typename?: 'Invitation', inviteeEmail: string, organization: { __typename?: 'Organization', name: string }, inviter: { __typename?: 'User', id: string, email: string, avatarUrl?: string | null, account: { __typename?: 'Account', name: string } } } };
|
||||
export type InvitationQuery = { __typename?: 'Query', invitation: { __typename?: 'Invitation', inviteeEmail: string, id: string } };
|
||||
|
||||
export type InviteUserMutationVariables = Exact<{
|
||||
input: InviteUserInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type InviteUserMutation = { __typename?: 'Mutation', inviteUser: { __typename?: 'Invitation', inviteeEmail: string, inviter: { __typename?: 'User', id: string, email: string, avatarUrl?: string | null, account: { __typename?: 'Account', name: string } }, organization: { __typename?: 'Organization', account: { __typename?: 'Account', name: string } } } };
|
||||
export type InviteUserMutation = { __typename?: 'Mutation', inviteUser: { __typename?: 'Invitation', inviteeEmail: string, id: string } };
|
||||
|
||||
export type MeQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
@ -320,7 +336,9 @@ export type OrganizationQueryVariables = Exact<{
|
|||
}>;
|
||||
|
||||
|
||||
export type OrganizationQuery = { __typename?: 'Query', organization?: { __typename?: 'Organization', id: string, users: Array<{ __typename?: 'User', id: string, email: string, avatarUrl?: string | null, account: { __typename?: 'Account', name: string } }>, admins: Array<{ __typename?: 'User', id: string, email: string, avatarUrl?: string | null, account: { __typename?: 'Account', name: string } }> } | null };
|
||||
export type OrganizationQuery = { __typename?: 'Query', organization?: { __typename?: 'Organization', id: string, users: Array<{ __typename?: 'User', id: string, email: string, avatarUrl?: string | null, account: { __typename?: 'Account', name: string } }>, admins: Array<{ __typename?: 'User', id: string, email: string, avatarUrl?: string | null, account: { __typename?: 'Account', name: string } }>, invitations: Array<{ __typename?: 'Invitation', inviteeEmail: string, id: string }> } | null };
|
||||
|
||||
export type PendingInvitationFragment = { __typename?: 'Invitation', inviteeEmail: string, id: string };
|
||||
|
||||
export type ProjectQueryVariables = Exact<{
|
||||
name: Scalars['String'];
|
||||
|
@ -337,6 +355,13 @@ export type RemoveUserMutationVariables = Exact<{
|
|||
|
||||
export type RemoveUserMutation = { __typename?: 'Mutation', removeUser: { __typename?: 'User', id: string, email: string, avatarUrl?: string | null, account: { __typename?: 'Account', name: string } } };
|
||||
|
||||
export type ResendInviteMutationVariables = Exact<{
|
||||
input: ResendInviteInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type ResendInviteMutation = { __typename?: 'Mutation', resendInvite: { __typename?: 'Invitation', inviteeEmail: string } };
|
||||
|
||||
export type S3BucketInfoFragment = { __typename?: 'S3Bucket', id: string, name: string, accessKeyId: string, secretAccessKey?: string | null, accountId: string, region: string };
|
||||
|
||||
export type S3BucketsQueryVariables = Exact<{
|
||||
|
@ -355,6 +380,12 @@ export type UpdateS3BucketMutation = { __typename?: 'Mutation', updateS3Bucket:
|
|||
|
||||
export type UserBasicInfoFragment = { __typename?: 'User', id: string, email: string, avatarUrl?: string | null, account: { __typename?: 'Account', name: string } };
|
||||
|
||||
export const PendingInvitationFragmentDoc = gql`
|
||||
fragment PendingInvitation on Invitation {
|
||||
inviteeEmail
|
||||
id
|
||||
}
|
||||
`;
|
||||
export const S3BucketInfoFragmentDoc = gql`
|
||||
fragment S3BucketInfo on S3Bucket {
|
||||
id
|
||||
|
@ -549,16 +580,10 @@ export type CreateS3BucketMutationOptions = Apollo.BaseMutationOptions<CreateS3B
|
|||
export const InvitationDocument = gql`
|
||||
query Invitation($token: String!) {
|
||||
invitation(token: $token) {
|
||||
organization {
|
||||
name
|
||||
}
|
||||
inviteeEmail
|
||||
inviter {
|
||||
...UserBasicInfo
|
||||
}
|
||||
...PendingInvitation
|
||||
}
|
||||
}
|
||||
${UserBasicInfoFragmentDoc}`;
|
||||
${PendingInvitationFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useInvitationQuery__
|
||||
|
@ -590,18 +615,10 @@ export type InvitationQueryResult = Apollo.QueryResult<InvitationQuery, Invitati
|
|||
export const InviteUserDocument = gql`
|
||||
mutation InviteUser($input: InviteUserInput!) {
|
||||
inviteUser(input: $input) {
|
||||
inviteeEmail
|
||||
inviter {
|
||||
...UserBasicInfo
|
||||
}
|
||||
organization {
|
||||
account {
|
||||
name
|
||||
}
|
||||
}
|
||||
...PendingInvitation
|
||||
}
|
||||
}
|
||||
${UserBasicInfoFragmentDoc}`;
|
||||
${PendingInvitationFragmentDoc}`;
|
||||
export type InviteUserMutationFn = Apollo.MutationFunction<InviteUserMutation, InviteUserMutationVariables>;
|
||||
|
||||
/**
|
||||
|
@ -750,9 +767,13 @@ export const OrganizationDocument = gql`
|
|||
admins {
|
||||
...UserBasicInfo
|
||||
}
|
||||
invitations {
|
||||
...PendingInvitation
|
||||
}
|
||||
}
|
||||
}
|
||||
${UserBasicInfoFragmentDoc}`;
|
||||
${UserBasicInfoFragmentDoc}
|
||||
${PendingInvitationFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useOrganizationQuery__
|
||||
|
@ -867,6 +888,39 @@ export function useRemoveUserMutation(baseOptions?: Apollo.MutationHookOptions<R
|
|||
export type RemoveUserMutationHookResult = ReturnType<typeof useRemoveUserMutation>;
|
||||
export type RemoveUserMutationResult = Apollo.MutationResult<RemoveUserMutation>;
|
||||
export type RemoveUserMutationOptions = Apollo.BaseMutationOptions<RemoveUserMutation, RemoveUserMutationVariables>;
|
||||
export const ResendInviteDocument = gql`
|
||||
mutation ResendInvite($input: ResendInviteInput!) {
|
||||
resendInvite(input: $input) {
|
||||
inviteeEmail
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type ResendInviteMutationFn = Apollo.MutationFunction<ResendInviteMutation, ResendInviteMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useResendInviteMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useResendInviteMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useResendInviteMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [resendInviteMutation, { data, loading, error }] = useResendInviteMutation({
|
||||
* variables: {
|
||||
* input: // value for 'input'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useResendInviteMutation(baseOptions?: Apollo.MutationHookOptions<ResendInviteMutation, ResendInviteMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<ResendInviteMutation, ResendInviteMutationVariables>(ResendInviteDocument, options);
|
||||
}
|
||||
export type ResendInviteMutationHookResult = ReturnType<typeof useResendInviteMutation>;
|
||||
export type ResendInviteMutationResult = Apollo.MutationResult<ResendInviteMutation>;
|
||||
export type ResendInviteMutationOptions = Apollo.BaseMutationOptions<ResendInviteMutation, ResendInviteMutationVariables>;
|
||||
export const S3BucketsDocument = gql`
|
||||
query S3Buckets($accountName: String!) {
|
||||
s3Buckets(accountName: $accountName) {
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import { Organization } from '@/graphql/types';
|
||||
import {
|
||||
mapPendingInvitation,
|
||||
PendingInvitation,
|
||||
} from './PendingInvitation';
|
||||
import { mapUserBasicInfo, UserBasicInfo } from './UserBasicInfo';
|
||||
|
||||
export interface OrganizationDetail {
|
||||
id: string;
|
||||
pendingInvitations: PendingInvitation[];
|
||||
admins: UserBasicInfo[];
|
||||
users: UserBasicInfo[];
|
||||
}
|
||||
|
||||
export const mapOrganizationDetail = ({
|
||||
id,
|
||||
invitations,
|
||||
admins,
|
||||
users,
|
||||
}: Organization) => {
|
||||
return {
|
||||
id,
|
||||
pendingInvitations: invitations.map((pendingInvitation) =>
|
||||
mapPendingInvitation(pendingInvitation),
|
||||
),
|
||||
admins: admins.map((admin) => mapUserBasicInfo(admin)),
|
||||
users: users.map((user) => mapUserBasicInfo(user)),
|
||||
} as OrganizationDetail;
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
import { PendingInvitationFragment } from '@/graphql/types';
|
||||
|
||||
export interface PendingInvitation {
|
||||
id: string;
|
||||
inviteeEmail: string;
|
||||
}
|
||||
|
||||
export const mapPendingInvitation = ({
|
||||
id,
|
||||
inviteeEmail,
|
||||
}: PendingInvitationFragment) => {
|
||||
return {
|
||||
id,
|
||||
inviteeEmail,
|
||||
} as PendingInvitation;
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
import { UserBasicInfoFragment } from '@/graphql/types';
|
||||
|
||||
export interface UserBasicInfo {
|
||||
id: string;
|
||||
email: string;
|
||||
avatarUrl: string;
|
||||
accountName: string;
|
||||
}
|
||||
|
||||
export const mapUserBasicInfo = ({
|
||||
id,
|
||||
email,
|
||||
avatarUrl,
|
||||
account: { name },
|
||||
}: UserBasicInfoFragment) => {
|
||||
return {
|
||||
id,
|
||||
email,
|
||||
avatarUrl,
|
||||
accountName: name,
|
||||
} as UserBasicInfo;
|
||||
};
|
|
@ -3,3 +3,5 @@ export type { User } from './User';
|
|||
export type { Account } from './Account';
|
||||
export type { S3Bucket } from './S3Bucket';
|
||||
export { mapS3Bucket } from './S3Bucket';
|
||||
export type { OrganizationDetail } from './OrganizationDetail';
|
||||
export { mapOrganizationDetail } from './OrganizationDetail';
|
||||
|
|
|
@ -2,15 +2,19 @@ import { ApolloClient } from '@apollo/client';
|
|||
import { makeAutoObservable, runInAction } from 'mobx';
|
||||
import {
|
||||
ChangeUserRoleDocument,
|
||||
OrganizationQuery,
|
||||
OrganizationDocument,
|
||||
Role,
|
||||
RemoveUserDocument,
|
||||
InviteUserDocument,
|
||||
} from '../graphql/types';
|
||||
ResendInviteMutation,
|
||||
ResendInviteDocument,
|
||||
InviteUserMutation,
|
||||
} from '@/graphql/types';
|
||||
import { OrganizationDetail, mapOrganizationDetail } from '@/models';
|
||||
import { mapPendingInvitation } from '@/models/PendingInvitation';
|
||||
|
||||
class OrganizationStore {
|
||||
organization: OrganizationQuery['organization'];
|
||||
organization: OrganizationDetail | undefined;
|
||||
|
||||
client: ApolloClient<object>;
|
||||
|
||||
|
@ -36,7 +40,7 @@ class OrganizationStore {
|
|||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.account.name,
|
||||
name: user.accountName,
|
||||
avatarUrl: user.avatarUrl ?? undefined,
|
||||
role: Role.User,
|
||||
};
|
||||
|
@ -51,7 +55,7 @@ class OrganizationStore {
|
|||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.account.name,
|
||||
name: user.accountName,
|
||||
avatarUrl: user.avatarUrl ?? undefined,
|
||||
role: Role.Admin,
|
||||
};
|
||||
|
@ -131,7 +135,7 @@ class OrganizationStore {
|
|||
variables: { name: organizationName },
|
||||
});
|
||||
runInAction(() => {
|
||||
this.organization = data.organization;
|
||||
this.organization = mapOrganizationDetail(data.organization);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -139,7 +143,7 @@ class OrganizationStore {
|
|||
if (!this.organization) {
|
||||
return;
|
||||
}
|
||||
await this.client.mutate({
|
||||
const { data } = await this.client.mutate<InviteUserMutation>({
|
||||
mutation: InviteUserDocument,
|
||||
variables: {
|
||||
input: {
|
||||
|
@ -148,6 +152,24 @@ class OrganizationStore {
|
|||
},
|
||||
},
|
||||
});
|
||||
if (data === null || data === undefined) {
|
||||
return;
|
||||
}
|
||||
this.organization.pendingInvitations = [
|
||||
mapPendingInvitation(data.inviteUser),
|
||||
...this.organization.pendingInvitations,
|
||||
];
|
||||
}
|
||||
|
||||
async resendInvite(invitationId: string) {
|
||||
await this.client.mutate<ResendInviteMutation>({
|
||||
mutation: ResendInviteDocument,
|
||||
variables: {
|
||||
input: {
|
||||
invitationId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ module Mutations
|
|||
argument :organization_id, String, required: true
|
||||
|
||||
def resolve(attributes)
|
||||
OrganizationInviteService.call(**attributes, inviter: context[:current_user])
|
||||
OrganizationInviteService.new.invite(**attributes, inviter: context[:current_user])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
class ResendInvite < ::Mutations::BaseMutation
|
||||
argument :invitation_id, String, required: true
|
||||
|
||||
def resolve(attributes)
|
||||
OrganizationInviteService.new.resend_invite(**attributes)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,5 +6,6 @@ module Types
|
|||
field :inviter, UserType, null: false
|
||||
field :organization, OrganizationType, null: false
|
||||
field :token, String, null: false
|
||||
field :id, ID, null: false
|
||||
end
|
||||
end
|
||||
|
|
|
@ -22,6 +22,11 @@ module Types
|
|||
null: false,
|
||||
description: "Invite a user to a given organization",
|
||||
mutation: Mutations::InviteUser
|
||||
field :resend_invite,
|
||||
InvitationType,
|
||||
null: false,
|
||||
description: "Resend invite for a user to a given organization",
|
||||
mutation: Mutations::ResendInvite
|
||||
field :accept_invitation,
|
||||
OrganizationType,
|
||||
null: false,
|
||||
|
|
|
@ -7,5 +7,6 @@ module Types
|
|||
field :users, [UserType], null: false
|
||||
field :admins, [UserType], null: false
|
||||
field :name, String, null: false
|
||||
field :invitations, [InvitationType], null: false
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,7 @@ class Organization < ApplicationRecord
|
|||
|
||||
# Associations
|
||||
has_one :account, as: :owner, class_name: "Account", dependent: :destroy
|
||||
has_many :invitations, as: :organization, dependent: :destroy
|
||||
has_many :invitations, dependent: :destroy
|
||||
# Inspired from: https://github.com/RolifyCommunity/rolify/wiki/Usage#finding-roles-through-associations
|
||||
has_many :users, -> {
|
||||
where(roles: { name: :user }).where.not(roles: { name: :admin }) }, through: :roles, class_name: "User", source: :users
|
||||
|
@ -14,4 +14,8 @@ class Organization < ApplicationRecord
|
|||
def name
|
||||
account.name
|
||||
end
|
||||
|
||||
def pending_invitations
|
||||
invitations.where(accepted: false)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -21,7 +21,10 @@ class InvitationAcceptService < ApplicationService
|
|||
invitation = InvitationFetchService.call(token: token)
|
||||
raise Error::Unauthorized.new unless invitation.invitee_email == user.email
|
||||
|
||||
user.add_role(:user, invitation.organization)
|
||||
ActiveRecord::Base.transaction do
|
||||
user.add_role(:user, invitation.organization)
|
||||
invitation.delete
|
||||
end
|
||||
invitation.organization
|
||||
end
|
||||
end
|
||||
|
|
|
@ -21,29 +21,64 @@ class OrganizationInviteService < ApplicationService
|
|||
"Organization with id #{organization_id} was not found"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(inviter:, invitee_email:, organization_id:)
|
||||
super()
|
||||
@inviter = inviter
|
||||
@invitee_email = invitee_email
|
||||
@organization_id = organization_id
|
||||
end
|
||||
class InvitationNotFound < CloudError
|
||||
attr_reader :invitation_id
|
||||
|
||||
def call
|
||||
begin
|
||||
organization = Organization.find(organization_id)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
raise Error::OrganizationNotFound.new(organization_id)
|
||||
def initialize(invitation_id)
|
||||
@invitation_id = invitation_id
|
||||
end
|
||||
|
||||
def message
|
||||
"Invitation with id #{invitation_id} was not found"
|
||||
end
|
||||
end
|
||||
raise Error::Unauthorized unless OrganizationPolicy.new(inviter, organization).update?
|
||||
|
||||
class DuplicateInvitation < CloudError
|
||||
attr_reader :invitee_email, :organization_id
|
||||
|
||||
def initialize(invitee_email, organization_id)
|
||||
@invitee_email = invitee_email
|
||||
@organization_id = organization_id
|
||||
end
|
||||
|
||||
def message
|
||||
"User with email #{invitee_email} has already been to organization with id #{organization_id}. \
|
||||
Consider resending the invite instead."
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def resend_invite(invitation_id:)
|
||||
begin
|
||||
invitation = Invitation.find(invitation_id)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
raise Error::InvitationNotFound.new(invitation_id)
|
||||
end
|
||||
organization = find_organization(organization_id: invitation.organization.id, inviter: invitation.inviter)
|
||||
InvitationMailer
|
||||
.invitation_mail(
|
||||
inviter: invitation.inviter,
|
||||
invitee_email: invitation.invitee_email,
|
||||
organization: organization,
|
||||
token: invitation.token
|
||||
)
|
||||
.deliver_now
|
||||
invitation
|
||||
end
|
||||
|
||||
def invite(inviter:, invitee_email:, organization_id:)
|
||||
organization = find_organization(organization_id: organization_id, inviter: inviter)
|
||||
token = Devise.friendly_token.first(16)
|
||||
invitation = inviter.invitations.create!(
|
||||
invitee_email: invitee_email,
|
||||
organization_id: organization.id,
|
||||
token: token
|
||||
)
|
||||
begin
|
||||
invitation = inviter.invitations.create!(
|
||||
invitee_email: invitee_email,
|
||||
organization_id: organization.id,
|
||||
token: token
|
||||
)
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
raise Error::DuplicateInvitation.new(invitee_email, organization.id)
|
||||
end
|
||||
InvitationMailer
|
||||
.invitation_mail(
|
||||
inviter: inviter,
|
||||
|
@ -54,4 +89,15 @@ class OrganizationInviteService < ApplicationService
|
|||
.deliver_now
|
||||
invitation
|
||||
end
|
||||
|
||||
private def find_organization(organization_id:, inviter:)
|
||||
begin
|
||||
organization = Organization.find(organization_id)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
raise Error::OrganizationNotFound.new(organization_id)
|
||||
end
|
||||
raise Error::Unauthorized unless OrganizationPolicy.new(inviter, organization).update?
|
||||
|
||||
organization
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddInvitationIndex < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_index(:invitations, [:invitee_email, :organization_id], unique: true)
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.0].define(version: 2022_03_11_074508) do
|
||||
ActiveRecord::Schema[7.0].define(version: 2022_03_23_204451) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
|
@ -29,10 +29,11 @@ ActiveRecord::Schema[7.0].define(version: 2022_03_11_074508) do
|
|||
t.string "inviter_type", null: false
|
||||
t.bigint "inviter_id", null: false
|
||||
t.string "invitee_email", null: false
|
||||
t.bigint "organization_id", null: false
|
||||
t.string "token", limit: 100, null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.bigint "organization_id"
|
||||
t.index ["invitee_email", "organization_id"], name: "index_invitations_on_invitee_email_and_organization_id", unique: true
|
||||
t.index ["inviter_type", "inviter_id"], name: "index_invitations_on_inviter"
|
||||
t.index ["organization_id"], name: "index_invitations_on_organization_id"
|
||||
t.index ["token"], name: "index_invitations_on_token", unique: true
|
||||
|
|
|
@ -72,6 +72,7 @@ input CreateS3BucketInput {
|
|||
}
|
||||
|
||||
type Invitation {
|
||||
id: ID!
|
||||
inviteeEmail: ID!
|
||||
inviter: User!
|
||||
organization: Organization!
|
||||
|
@ -161,6 +162,16 @@ type Mutation {
|
|||
input: RemoveUserInput!
|
||||
): User!
|
||||
|
||||
"""
|
||||
Resend invite for a user to a given organization
|
||||
"""
|
||||
resendInvite(
|
||||
"""
|
||||
Parameters for ResendInvite
|
||||
"""
|
||||
input: ResendInviteInput!
|
||||
): Invitation!
|
||||
|
||||
"""
|
||||
Update S3 bucket
|
||||
"""
|
||||
|
@ -176,6 +187,7 @@ type Organization {
|
|||
account: Account!
|
||||
admins: [User!]!
|
||||
id: ID!
|
||||
invitations: [Invitation!]!
|
||||
name: String!
|
||||
users: [User!]!
|
||||
}
|
||||
|
@ -246,6 +258,17 @@ input RemoveUserInput {
|
|||
userId: ID!
|
||||
}
|
||||
|
||||
"""
|
||||
Autogenerated input type of ResendInvite
|
||||
"""
|
||||
input ResendInviteInput {
|
||||
"""
|
||||
A unique identifier for the client performing the mutation.
|
||||
"""
|
||||
clientMutationId: String
|
||||
invitationId: String!
|
||||
}
|
||||
|
||||
enum Role {
|
||||
admin
|
||||
user
|
||||
|
|
|
@ -458,6 +458,24 @@
|
|||
"name": "Invitation",
|
||||
"description": null,
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"description": null,
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "inviteeEmail",
|
||||
"description": null,
|
||||
|
@ -829,6 +847,39 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "resendInvite",
|
||||
"description": "Resend invite for a user to a given organization",
|
||||
"args": [
|
||||
{
|
||||
"name": "input",
|
||||
"description": "Parameters for ResendInvite",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "ResendInviteInput",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Invitation",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "updateS3Bucket",
|
||||
"description": "Update S3 bucket",
|
||||
|
@ -937,6 +988,32 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "invitations",
|
||||
"description": null,
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Invitation",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"description": null,
|
||||
|
@ -1437,6 +1514,45 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "ResendInviteInput",
|
||||
"description": "Autogenerated input type of ResendInvite",
|
||||
"fields": null,
|
||||
"inputFields": [
|
||||
{
|
||||
"name": "clientMutationId",
|
||||
"description": "A unique identifier for the client performing the mutation.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "invitationId",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "ENUM",
|
||||
"name": "Role",
|
||||
|
|
|
@ -10,7 +10,7 @@ class InvitationAcceptServiceTest < ActiveSupport::TestCase
|
|||
token = Devise.friendly_token.first(8)
|
||||
organization = Organization.create!
|
||||
Account.create!(owner: organization, name: "tuist")
|
||||
inviter.invitations.create!(invitee_email: user.email, token: token, organization: organization)
|
||||
invitation = inviter.invitations.create!(invitee_email: user.email, token: token, organization: organization)
|
||||
|
||||
# When
|
||||
got = InvitationAcceptService.call(token: token, user: user)
|
||||
|
@ -18,6 +18,9 @@ class InvitationAcceptServiceTest < ActiveSupport::TestCase
|
|||
# Then
|
||||
assert user.has_role?(:user, organization)
|
||||
assert organization, got
|
||||
assert_raises(ActiveRecord::RecordNotFound) do
|
||||
Invitation.find(invitation.id)
|
||||
end
|
||||
end
|
||||
|
||||
test "fails with not authorized error when invitation email and user email mismatch" do
|
||||
|
@ -26,7 +29,8 @@ class InvitationAcceptServiceTest < ActiveSupport::TestCase
|
|||
inviter = User.create!(email: "test1@cloud.tuist.io", password: Devise.friendly_token.first(16))
|
||||
token = Devise.friendly_token.first(8)
|
||||
organization = Organization.create!
|
||||
inviter.invitations.create!(invitee_email: "test2@cloud.tuist.io", token: token, organization: organization)
|
||||
inviter.invitations.create!(invitee_email: "test2@cloud.tuist.io", token: token,
|
||||
organization: organization)
|
||||
|
||||
# When / Then
|
||||
assert_raises(InvitationAcceptService::Error::Unauthorized) do
|
||||
|
|
|
@ -12,7 +12,7 @@ class OrganizationInviteServiceTest < ActiveSupport::TestCase
|
|||
invitee_email = "test1@cloud.tuist.io"
|
||||
|
||||
# When
|
||||
got = OrganizationInviteService.call(
|
||||
got = OrganizationInviteService.new.invite(
|
||||
inviter: inviter,
|
||||
invitee_email: invitee_email,
|
||||
organization_id: organization.id
|
||||
|
@ -23,6 +23,34 @@ class OrganizationInviteServiceTest < ActiveSupport::TestCase
|
|||
assert_equal got.organization, organization
|
||||
end
|
||||
|
||||
test "invite a user to two organizations" do
|
||||
# Given
|
||||
inviter = User.create!(email: "test@cloud.tuist.io", password: Devise.friendly_token.first(16))
|
||||
organization_one = Organization.create!
|
||||
organization_two = Organization.create!
|
||||
Account.create!(owner: organization_one, name: "tuist_one")
|
||||
inviter.add_role(:admin, organization_one)
|
||||
Account.create!(owner: organization_two, name: "tuist_two")
|
||||
inviter.add_role(:admin, organization_two)
|
||||
invitee_email = "test1@cloud.tuist.io"
|
||||
|
||||
# When
|
||||
OrganizationInviteService.new.invite(
|
||||
inviter: inviter,
|
||||
invitee_email: invitee_email,
|
||||
organization_id: organization_one.id
|
||||
)
|
||||
got = OrganizationInviteService.new.invite(
|
||||
inviter: inviter,
|
||||
invitee_email: invitee_email,
|
||||
organization_id: organization_two.id
|
||||
)
|
||||
# Then
|
||||
assert_equal got.inviter, inviter
|
||||
assert_equal got.invitee_email, invitee_email
|
||||
assert_equal got.organization, organization_two
|
||||
end
|
||||
|
||||
test "inviting a user to the organization if user is not admin fails" do
|
||||
# Given
|
||||
inviter = User.create!(email: "test@cloud.tuist.io", password: Devise.friendly_token.first(16))
|
||||
|
@ -31,7 +59,7 @@ class OrganizationInviteServiceTest < ActiveSupport::TestCase
|
|||
|
||||
# When / Then
|
||||
assert_raises(OrganizationInviteService::Error::Unauthorized) do
|
||||
OrganizationInviteService.call(
|
||||
OrganizationInviteService.new.invite(
|
||||
inviter: inviter,
|
||||
invitee_email: "test1@cloud.tuist.io",
|
||||
organization_id: organization.id
|
||||
|
@ -45,11 +73,74 @@ class OrganizationInviteServiceTest < ActiveSupport::TestCase
|
|||
|
||||
# When / Then
|
||||
assert_raises(OrganizationInviteService::Error::OrganizationNotFound) do
|
||||
OrganizationInviteService.call(
|
||||
OrganizationInviteService.new.invite(
|
||||
inviter: inviter,
|
||||
invitee_email: "test1@cloud.tuist.io",
|
||||
organization_id: "1"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
test "inviting user to the same organization twice fails" do
|
||||
# Given
|
||||
inviter = User.create!(email: "test@cloud.tuist.io", password: Devise.friendly_token.first(16))
|
||||
organization = Organization.create!
|
||||
Account.create!(owner: organization, name: "tuist")
|
||||
inviter.add_role(:admin, organization)
|
||||
invitee_email = "test1@cloud.tuist.io"
|
||||
|
||||
# When / Then
|
||||
OrganizationInviteService.new.invite(
|
||||
inviter: inviter,
|
||||
invitee_email: invitee_email,
|
||||
organization_id: organization.id
|
||||
)
|
||||
assert_raises(OrganizationInviteService::Error::DuplicateInvitation) do
|
||||
OrganizationInviteService.new.invite(
|
||||
inviter: inviter,
|
||||
invitee_email: invitee_email,
|
||||
organization_id: organization.id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
test "resend invitation" do
|
||||
# Given
|
||||
invitee_email = "test1@cloud.tuist.io"
|
||||
organization = Organization.create!
|
||||
Account.create!(owner: organization, name: "tuist")
|
||||
inviter = User.create!(email: "test@cloud.tuist.io", password: Devise.friendly_token.first(16))
|
||||
inviter.add_role(:admin, organization)
|
||||
invitation = inviter.invitations.create!(
|
||||
invitee_email: invitee_email,
|
||||
organization_id: organization.id,
|
||||
token: "token"
|
||||
)
|
||||
InvitationMailer.any_instance.expects(:invitation_mail).once
|
||||
|
||||
# When
|
||||
got = OrganizationInviteService.new.resend_invite(
|
||||
invitation_id: invitation.id,
|
||||
)
|
||||
|
||||
# Then
|
||||
assert_equal got.inviter, inviter
|
||||
assert_equal got.invitee_email, invitee_email
|
||||
assert_equal got.organization, organization
|
||||
end
|
||||
|
||||
test "resend invitation fails when not found" do
|
||||
# Given
|
||||
organization = Organization.create!
|
||||
Account.create!(owner: organization, name: "tuist")
|
||||
inviter = User.create!(email: "test@cloud.tuist.io", password: Devise.friendly_token.first(16))
|
||||
inviter.add_role(:admin, organization)
|
||||
|
||||
# When / Then
|
||||
assert_raises(OrganizationInviteService::Error::InvitationNotFound) do
|
||||
OrganizationInviteService.new.resend_invite(
|
||||
invitation_id: 0
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue