mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-10 05:29:08 +08:00
feat(server): delay subscription after invitation accepted or approved (#11992)
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
AFFiNELogger,
|
||||
CacheInterceptor,
|
||||
CloudThrottlerGuard,
|
||||
EventBus,
|
||||
GlobalExceptionFilter,
|
||||
JobQueue,
|
||||
OneMB,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
import { SocketIoAdapter } from '../../base/websocket';
|
||||
import { AuthGuard, AuthService } from '../../core/auth';
|
||||
import { Mailer } from '../../core/mail';
|
||||
import { Models } from '../../models';
|
||||
import {
|
||||
createFactory,
|
||||
MockedUser,
|
||||
@@ -43,6 +45,8 @@ export class TestingApp extends NestApplication {
|
||||
create = createFactory(this.get(PrismaClient, { strict: false }));
|
||||
mails = this.get(Mailer, { strict: false }) as MockMailer;
|
||||
queue = this.get(JobQueue, { strict: false }) as MockJobQueue;
|
||||
eventBus = this.get(EventBus, { strict: false });
|
||||
models = this.get(Models, { strict: false });
|
||||
|
||||
get url() {
|
||||
const server = this.getHttpServer();
|
||||
|
||||
@@ -1,456 +0,0 @@
|
||||
import {
|
||||
acceptInviteByInviteIdMutation,
|
||||
createInviteLinkMutation,
|
||||
getInviteInfoQuery,
|
||||
getMembersByWorkspaceIdQuery,
|
||||
inviteByEmailMutation,
|
||||
leaveWorkspaceMutation,
|
||||
revokeMemberPermissionMutation,
|
||||
WorkspaceInviteLinkExpireTime,
|
||||
WorkspaceMemberStatus,
|
||||
} from '@affine/graphql';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
import { Models } from '../../../models';
|
||||
import { Mockers } from '../../mocks';
|
||||
import { app, e2e } from '../test';
|
||||
|
||||
e2e('should invite a user', async t => {
|
||||
const u2 = await app.signup();
|
||||
const owner = await app.signup();
|
||||
|
||||
const workspace = await app.create(Mockers.Workspace, {
|
||||
owner: { id: owner.id },
|
||||
});
|
||||
|
||||
const result = await app.gql({
|
||||
query: inviteByEmailMutation,
|
||||
variables: {
|
||||
email: u2.email,
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
});
|
||||
t.truthy(result, 'failed to invite user');
|
||||
// add invitation notification job
|
||||
const invitationNotification = app.queue.last('notification.sendInvitation');
|
||||
t.is(invitationNotification.payload.inviterId, owner.id);
|
||||
t.is(invitationNotification.payload.inviteId, result.invite);
|
||||
|
||||
// invitation status is pending
|
||||
const { getInviteInfo } = await app.gql({
|
||||
query: getInviteInfoQuery,
|
||||
variables: {
|
||||
inviteId: result.invite,
|
||||
},
|
||||
});
|
||||
t.is(getInviteInfo.status, WorkspaceMemberStatus.Pending);
|
||||
|
||||
// u2 accept invite
|
||||
await app.switchUser(u2);
|
||||
await app.gql({
|
||||
query: acceptInviteByInviteIdMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
inviteId: result.invite,
|
||||
},
|
||||
});
|
||||
|
||||
// invitation status is accepted
|
||||
const { getInviteInfo: getInviteInfo2 } = await app.gql({
|
||||
query: getInviteInfoQuery,
|
||||
variables: {
|
||||
inviteId: result.invite,
|
||||
},
|
||||
});
|
||||
t.is(getInviteInfo2.status, WorkspaceMemberStatus.Accepted);
|
||||
});
|
||||
|
||||
e2e('should leave a workspace', async t => {
|
||||
const u2 = await app.signup();
|
||||
const owner = await app.signup();
|
||||
|
||||
const workspace = await app.create(Mockers.Workspace, {
|
||||
owner: { id: owner.id },
|
||||
});
|
||||
await app.create(Mockers.WorkspaceUser, {
|
||||
workspaceId: workspace.id,
|
||||
userId: u2.id,
|
||||
});
|
||||
|
||||
await app.switchUser(u2.id);
|
||||
const { leaveWorkspace } = await app.gql({
|
||||
query: leaveWorkspaceMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
});
|
||||
|
||||
t.true(leaveWorkspace, 'failed to leave workspace');
|
||||
});
|
||||
|
||||
e2e('should revoke a user', async t => {
|
||||
const u2 = await app.signup();
|
||||
const owner = await app.signup();
|
||||
|
||||
const workspace = await app.create(Mockers.Workspace, {
|
||||
owner: { id: owner.id },
|
||||
});
|
||||
await app.create(Mockers.WorkspaceUser, {
|
||||
workspaceId: workspace.id,
|
||||
userId: u2.id,
|
||||
});
|
||||
|
||||
const { revoke } = await app.gql({
|
||||
query: revokeMemberPermissionMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
userId: u2.id,
|
||||
},
|
||||
});
|
||||
t.true(revoke, 'failed to revoke user');
|
||||
});
|
||||
|
||||
e2e('should revoke a user on under review', async t => {
|
||||
const user = await app.signup();
|
||||
const owner = await app.signup();
|
||||
|
||||
const workspace = await app.create(Mockers.Workspace, {
|
||||
owner: { id: owner.id },
|
||||
});
|
||||
await app.create(Mockers.WorkspaceUser, {
|
||||
workspaceId: workspace.id,
|
||||
userId: user.id,
|
||||
status: WorkspaceMemberStatus.UnderReview,
|
||||
});
|
||||
|
||||
const { revoke } = await app.gql({
|
||||
query: revokeMemberPermissionMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
t.true(revoke, 'failed to revoke user');
|
||||
const requestDeclinedNotification = app.queue.last(
|
||||
'notification.sendInvitationReviewDeclined'
|
||||
);
|
||||
t.truthy(requestDeclinedNotification);
|
||||
t.deepEqual(
|
||||
requestDeclinedNotification.payload,
|
||||
{
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
reviewerId: owner.id,
|
||||
},
|
||||
'should send review declined notification'
|
||||
);
|
||||
});
|
||||
|
||||
e2e('should create user if not exist', async t => {
|
||||
const owner = await app.signup();
|
||||
|
||||
const workspace = await app.create(Mockers.Workspace, {
|
||||
owner: { id: owner.id },
|
||||
});
|
||||
|
||||
const email = faker.internet.email();
|
||||
await app.gql({
|
||||
query: inviteByEmailMutation,
|
||||
variables: {
|
||||
email,
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
});
|
||||
|
||||
const u2 = await app.get(Models).user.getUserByEmail(email);
|
||||
t.truthy(u2, 'failed to create user');
|
||||
});
|
||||
|
||||
e2e('should invite a user by link', async t => {
|
||||
const u2 = await app.signup();
|
||||
const owner = await app.signup();
|
||||
|
||||
const workspace = await app.create(Mockers.Workspace, {
|
||||
owner: { id: owner.id },
|
||||
});
|
||||
|
||||
const invite1 = await app.gql({
|
||||
query: inviteByEmailMutation,
|
||||
variables: {
|
||||
email: u2.email,
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
});
|
||||
|
||||
await app.switchUser(u2);
|
||||
const accept = await app.gql({
|
||||
query: acceptInviteByInviteIdMutation,
|
||||
variables: {
|
||||
inviteId: invite1.invite,
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
});
|
||||
t.true(accept.acceptInviteById, 'failed to accept invite');
|
||||
|
||||
await app.switchUser(owner);
|
||||
const invite2 = await app.gql({
|
||||
query: inviteByEmailMutation,
|
||||
variables: {
|
||||
email: u2.email,
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
});
|
||||
|
||||
t.is(
|
||||
invite2.invite,
|
||||
invite1.invite,
|
||||
'repeat the invitation must return same id'
|
||||
);
|
||||
|
||||
const member = await app
|
||||
.get(Models)
|
||||
.workspaceUser.getActive(workspace.id, u2.id);
|
||||
t.truthy(member, 'failed to invite user');
|
||||
t.is(member!.id, invite1.invite, 'failed to check invite id');
|
||||
});
|
||||
|
||||
e2e('should send invitation notification and leave email', async t => {
|
||||
const u2 = await app.signup();
|
||||
const owner = await app.signup();
|
||||
|
||||
const workspace = await app.create(Mockers.Workspace, {
|
||||
owner: { id: owner.id },
|
||||
});
|
||||
const invite = await app.gql({
|
||||
query: inviteByEmailMutation,
|
||||
variables: {
|
||||
email: u2.email,
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
});
|
||||
|
||||
const invitationNotification = app.queue.last('notification.sendInvitation');
|
||||
t.is(invitationNotification.payload.inviterId, owner.id);
|
||||
t.is(invitationNotification.payload.inviteId, invite.invite);
|
||||
|
||||
await app.switchUser(u2);
|
||||
const accept = await app.gql({
|
||||
query: acceptInviteByInviteIdMutation,
|
||||
variables: {
|
||||
inviteId: invite.invite,
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
});
|
||||
t.true(accept.acceptInviteById, 'failed to accept invite');
|
||||
|
||||
const acceptedNotification = app.queue.last(
|
||||
'notification.sendInvitationAccepted'
|
||||
);
|
||||
t.is(acceptedNotification.payload.inviterId, owner.id);
|
||||
t.is(acceptedNotification.payload.inviteId, invite.invite);
|
||||
|
||||
const leave = await app.gql({
|
||||
query: leaveWorkspaceMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
sendLeaveMail: true,
|
||||
},
|
||||
});
|
||||
t.true(leave.leaveWorkspace, 'failed to leave workspace');
|
||||
|
||||
const leaveMail = app.mails.last('MemberLeave');
|
||||
|
||||
t.is(leaveMail.to, owner.email);
|
||||
t.is(leaveMail.props.user.$$userId, u2.id);
|
||||
});
|
||||
|
||||
e2e('should support pagination for member', async t => {
|
||||
const u1 = await app.signup();
|
||||
const u2 = await app.signup();
|
||||
const owner = await app.signup();
|
||||
|
||||
const workspace = await app.create(Mockers.Workspace, {
|
||||
owner: { id: owner.id },
|
||||
});
|
||||
await app.create(Mockers.WorkspaceUser, {
|
||||
workspaceId: workspace.id,
|
||||
userId: u1.id,
|
||||
});
|
||||
await app.create(Mockers.WorkspaceUser, {
|
||||
workspaceId: workspace.id,
|
||||
userId: u2.id,
|
||||
});
|
||||
|
||||
let result = await app.gql({
|
||||
query: getMembersByWorkspaceIdQuery,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
skip: 0,
|
||||
take: 2,
|
||||
},
|
||||
});
|
||||
t.is(result.workspace.memberCount, 3);
|
||||
t.is(result.workspace.members.length, 2);
|
||||
|
||||
result = await app.gql({
|
||||
query: getMembersByWorkspaceIdQuery,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
skip: 2,
|
||||
take: 2,
|
||||
},
|
||||
});
|
||||
t.is(result.workspace.memberCount, 3);
|
||||
t.is(result.workspace.members.length, 1);
|
||||
|
||||
result = await app.gql({
|
||||
query: getMembersByWorkspaceIdQuery,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
skip: 3,
|
||||
take: 2,
|
||||
},
|
||||
});
|
||||
t.is(result.workspace.memberCount, 3);
|
||||
t.is(result.workspace.members.length, 0);
|
||||
});
|
||||
|
||||
e2e('should limit member count correctly', async t => {
|
||||
const owner = await app.signup();
|
||||
|
||||
const workspace = await app.create(Mockers.Workspace, {
|
||||
owner: { id: owner.id },
|
||||
});
|
||||
await Promise.all(
|
||||
Array.from({ length: 10 }).map(async () => {
|
||||
const user = await app.signup();
|
||||
await app.create(Mockers.WorkspaceUser, {
|
||||
workspaceId: workspace.id,
|
||||
userId: user.id,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
await app.switchUser(owner);
|
||||
const result = await app.gql({
|
||||
query: getMembersByWorkspaceIdQuery,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
skip: 0,
|
||||
take: 10,
|
||||
},
|
||||
});
|
||||
t.is(result.workspace.memberCount, 11);
|
||||
t.is(result.workspace.members.length, 10);
|
||||
});
|
||||
|
||||
e2e('should get invite link info with status', async t => {
|
||||
const owner = await app.signup();
|
||||
|
||||
const workspace = await app.create(Mockers.Workspace, {
|
||||
owner: { id: owner.id },
|
||||
});
|
||||
|
||||
await app.login(owner);
|
||||
const { createInviteLink } = await app.gql({
|
||||
query: createInviteLinkMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
expireTime: WorkspaceInviteLinkExpireTime.OneDay,
|
||||
},
|
||||
});
|
||||
t.truthy(createInviteLink, 'failed to create invite link');
|
||||
const link = createInviteLink.link;
|
||||
const inviteId = link.split('/').pop()!;
|
||||
|
||||
// owner/member see accept status
|
||||
const { getInviteInfo } = await app.gql({
|
||||
query: getInviteInfoQuery,
|
||||
variables: {
|
||||
inviteId,
|
||||
},
|
||||
});
|
||||
t.truthy(getInviteInfo, 'failed to get invite info');
|
||||
t.is(getInviteInfo.status, WorkspaceMemberStatus.Accepted);
|
||||
|
||||
// non-member see null status
|
||||
await app.signup();
|
||||
const { getInviteInfo: getInviteInfo2 } = await app.gql({
|
||||
query: getInviteInfoQuery,
|
||||
variables: {
|
||||
inviteId,
|
||||
},
|
||||
});
|
||||
t.truthy(getInviteInfo2, 'failed to get invite info');
|
||||
t.is(getInviteInfo2.status, null);
|
||||
|
||||
// pending-member see under review status
|
||||
await app.signup();
|
||||
await app.gql({
|
||||
query: acceptInviteByInviteIdMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
inviteId,
|
||||
},
|
||||
});
|
||||
const { getInviteInfo: getInviteInfo3 } = await app.gql({
|
||||
query: getInviteInfoQuery,
|
||||
variables: {
|
||||
inviteId,
|
||||
},
|
||||
});
|
||||
t.truthy(getInviteInfo3, 'failed to get invite info');
|
||||
t.is(getInviteInfo3.status, WorkspaceMemberStatus.UnderReview);
|
||||
});
|
||||
|
||||
e2e(
|
||||
'should accept invitation by link directly if status is pending',
|
||||
async t => {
|
||||
const owner = await app.create(Mockers.User);
|
||||
const member = await app.create(Mockers.User);
|
||||
|
||||
const workspace = await app.create(Mockers.Workspace, {
|
||||
owner: { id: owner.id },
|
||||
});
|
||||
|
||||
await app.login(owner);
|
||||
// create a pending invitation
|
||||
const invite = await app.gql({
|
||||
query: inviteByEmailMutation,
|
||||
variables: {
|
||||
email: member.email,
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
});
|
||||
t.truthy(invite, 'failed to create invitation');
|
||||
|
||||
const { createInviteLink } = await app.gql({
|
||||
query: createInviteLinkMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
expireTime: WorkspaceInviteLinkExpireTime.OneDay,
|
||||
},
|
||||
});
|
||||
t.truthy(createInviteLink, 'failed to create invite link');
|
||||
const link = createInviteLink.link;
|
||||
const inviteLinkId = link.split('/').pop()!;
|
||||
|
||||
// member accept invitation by link
|
||||
await app.login(member);
|
||||
await app.gql({
|
||||
query: acceptInviteByInviteIdMutation,
|
||||
variables: {
|
||||
inviteId: inviteLinkId,
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
});
|
||||
|
||||
const { getInviteInfo } = await app.gql({
|
||||
query: getInviteInfoQuery,
|
||||
variables: {
|
||||
inviteId: invite.invite,
|
||||
},
|
||||
});
|
||||
t.is(getInviteInfo.status, WorkspaceMemberStatus.Accepted);
|
||||
}
|
||||
);
|
||||
@@ -1,9 +1,459 @@
|
||||
import { getMembersByWorkspaceIdQuery } from '@affine/graphql';
|
||||
import {
|
||||
acceptInviteByInviteIdMutation,
|
||||
createInviteLinkMutation,
|
||||
getInviteInfoQuery,
|
||||
getMembersByWorkspaceIdQuery,
|
||||
inviteByEmailsMutation,
|
||||
leaveWorkspaceMutation,
|
||||
revokeMemberPermissionMutation,
|
||||
WorkspaceInviteLinkExpireTime,
|
||||
WorkspaceMemberStatus,
|
||||
} from '@affine/graphql';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
import { Models } from '../../../models';
|
||||
import { Mockers } from '../../mocks';
|
||||
import { app, e2e } from '../test';
|
||||
|
||||
async function createTeamWorkspace(quantity = 10) {
|
||||
const owner = await app.create(Mockers.User);
|
||||
|
||||
const workspace = await app.create(Mockers.Workspace, {
|
||||
owner: { id: owner.id },
|
||||
});
|
||||
await app.create(Mockers.TeamWorkspace, {
|
||||
id: workspace.id,
|
||||
quantity,
|
||||
});
|
||||
|
||||
return {
|
||||
owner,
|
||||
workspace,
|
||||
};
|
||||
}
|
||||
|
||||
e2e('should invite a user', async t => {
|
||||
const u2 = await app.signup();
|
||||
const owner = await app.signup();
|
||||
|
||||
const workspace = await app.create(Mockers.Workspace, {
|
||||
owner: { id: owner.id },
|
||||
});
|
||||
|
||||
const result = await app.gql({
|
||||
query: inviteByEmailsMutation,
|
||||
variables: {
|
||||
emails: [u2.email],
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
});
|
||||
t.truthy(result, 'failed to invite user');
|
||||
// add invitation notification job
|
||||
const invitationNotification = await app.queue.waitFor(
|
||||
'notification.sendInvitation'
|
||||
);
|
||||
t.is(invitationNotification.payload.inviterId, owner.id);
|
||||
t.is(
|
||||
invitationNotification.payload.inviteId,
|
||||
result.inviteMembers[0].inviteId!
|
||||
);
|
||||
|
||||
// invitation status is pending
|
||||
const { getInviteInfo } = await app.gql({
|
||||
query: getInviteInfoQuery,
|
||||
variables: {
|
||||
inviteId: invitationNotification.payload.inviteId,
|
||||
},
|
||||
});
|
||||
t.is(getInviteInfo.status, WorkspaceMemberStatus.Pending);
|
||||
|
||||
// u2 accept invite
|
||||
await app.switchUser(u2);
|
||||
await app.gql({
|
||||
query: acceptInviteByInviteIdMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
inviteId: invitationNotification.payload.inviteId,
|
||||
},
|
||||
});
|
||||
|
||||
// invitation status is accepted
|
||||
const { getInviteInfo: getInviteInfo2 } = await app.gql({
|
||||
query: getInviteInfoQuery,
|
||||
variables: {
|
||||
inviteId: invitationNotification.payload.inviteId,
|
||||
},
|
||||
});
|
||||
t.is(getInviteInfo2.status, WorkspaceMemberStatus.Accepted);
|
||||
});
|
||||
|
||||
e2e('should leave a workspace', async t => {
|
||||
const u2 = await app.signup();
|
||||
const owner = await app.signup();
|
||||
|
||||
const workspace = await app.create(Mockers.Workspace, {
|
||||
owner: { id: owner.id },
|
||||
});
|
||||
await app.create(Mockers.WorkspaceUser, {
|
||||
workspaceId: workspace.id,
|
||||
userId: u2.id,
|
||||
});
|
||||
|
||||
await app.switchUser(u2.id);
|
||||
const { leaveWorkspace } = await app.gql({
|
||||
query: leaveWorkspaceMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
});
|
||||
|
||||
t.true(leaveWorkspace, 'failed to leave workspace');
|
||||
|
||||
const leaveMail = await app.mails.waitFor('MemberLeave');
|
||||
|
||||
t.is(leaveMail.to, owner.email);
|
||||
t.is(leaveMail.props.user.$$userId, u2.id);
|
||||
});
|
||||
|
||||
e2e('should revoke a user', async t => {
|
||||
const u2 = await app.signup();
|
||||
const owner = await app.signup();
|
||||
|
||||
const workspace = await app.create(Mockers.Workspace, {
|
||||
owner: { id: owner.id },
|
||||
});
|
||||
await app.create(Mockers.WorkspaceUser, {
|
||||
workspaceId: workspace.id,
|
||||
userId: u2.id,
|
||||
});
|
||||
|
||||
const { revokeMember } = await app.gql({
|
||||
query: revokeMemberPermissionMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
userId: u2.id,
|
||||
},
|
||||
});
|
||||
t.true(revokeMember, 'failed to revoke user');
|
||||
});
|
||||
|
||||
e2e('should revoke a user on under review', async t => {
|
||||
const user = await app.signup();
|
||||
const owner = await app.signup();
|
||||
|
||||
const workspace = await app.create(Mockers.Workspace, {
|
||||
owner: { id: owner.id },
|
||||
});
|
||||
await app.create(Mockers.WorkspaceUser, {
|
||||
workspaceId: workspace.id,
|
||||
userId: user.id,
|
||||
status: WorkspaceMemberStatus.UnderReview,
|
||||
});
|
||||
|
||||
const { revokeMember } = await app.gql({
|
||||
query: revokeMemberPermissionMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
t.true(revokeMember, 'failed to revoke user');
|
||||
const requestDeclinedNotification = app.queue.last(
|
||||
'notification.sendInvitationReviewDeclined'
|
||||
);
|
||||
t.truthy(requestDeclinedNotification);
|
||||
t.deepEqual(
|
||||
requestDeclinedNotification.payload,
|
||||
{
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
reviewerId: owner.id,
|
||||
},
|
||||
'should send review declined notification'
|
||||
);
|
||||
});
|
||||
|
||||
e2e('should create user if not exist', async t => {
|
||||
const owner = await app.signup();
|
||||
|
||||
const workspace = await app.create(Mockers.Workspace, {
|
||||
owner: { id: owner.id },
|
||||
});
|
||||
|
||||
const email = faker.internet.email();
|
||||
await app.gql({
|
||||
query: inviteByEmailsMutation,
|
||||
variables: {
|
||||
emails: [email],
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
});
|
||||
|
||||
const u2 = await app.get(Models).user.getUserByEmail(email);
|
||||
t.truthy(u2, 'failed to create user');
|
||||
});
|
||||
|
||||
e2e('should support pagination for member', async t => {
|
||||
const u1 = await app.signup();
|
||||
const u2 = await app.signup();
|
||||
const owner = await app.signup();
|
||||
|
||||
const workspace = await app.create(Mockers.Workspace, {
|
||||
owner: { id: owner.id },
|
||||
});
|
||||
await app.create(Mockers.WorkspaceUser, {
|
||||
workspaceId: workspace.id,
|
||||
userId: u1.id,
|
||||
});
|
||||
await app.create(Mockers.WorkspaceUser, {
|
||||
workspaceId: workspace.id,
|
||||
userId: u2.id,
|
||||
});
|
||||
|
||||
let result = await app.gql({
|
||||
query: getMembersByWorkspaceIdQuery,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
skip: 0,
|
||||
take: 2,
|
||||
},
|
||||
});
|
||||
t.is(result.workspace.memberCount, 3);
|
||||
t.is(result.workspace.members.length, 2);
|
||||
|
||||
result = await app.gql({
|
||||
query: getMembersByWorkspaceIdQuery,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
skip: 2,
|
||||
take: 2,
|
||||
},
|
||||
});
|
||||
t.is(result.workspace.memberCount, 3);
|
||||
t.is(result.workspace.members.length, 1);
|
||||
|
||||
result = await app.gql({
|
||||
query: getMembersByWorkspaceIdQuery,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
skip: 3,
|
||||
take: 2,
|
||||
},
|
||||
});
|
||||
t.is(result.workspace.memberCount, 3);
|
||||
t.is(result.workspace.members.length, 0);
|
||||
});
|
||||
|
||||
e2e('should limit member count correctly', async t => {
|
||||
const owner = await app.signup();
|
||||
|
||||
const workspace = await app.create(Mockers.Workspace, {
|
||||
owner: { id: owner.id },
|
||||
});
|
||||
await Promise.all(
|
||||
Array.from({ length: 10 }).map(async () => {
|
||||
const user = await app.signup();
|
||||
await app.create(Mockers.WorkspaceUser, {
|
||||
workspaceId: workspace.id,
|
||||
userId: user.id,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
await app.switchUser(owner);
|
||||
const result = await app.gql({
|
||||
query: getMembersByWorkspaceIdQuery,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
skip: 0,
|
||||
take: 10,
|
||||
},
|
||||
});
|
||||
t.is(result.workspace.memberCount, 11);
|
||||
t.is(result.workspace.members.length, 10);
|
||||
});
|
||||
|
||||
e2e('should get invite link info with status', async t => {
|
||||
const owner = await app.signup();
|
||||
|
||||
const workspace = await app.create(Mockers.Workspace, {
|
||||
owner: { id: owner.id },
|
||||
});
|
||||
|
||||
await app.login(owner);
|
||||
const { createInviteLink } = await app.gql({
|
||||
query: createInviteLinkMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
expireTime: WorkspaceInviteLinkExpireTime.OneDay,
|
||||
},
|
||||
});
|
||||
t.truthy(createInviteLink, 'failed to create invite link');
|
||||
const link = createInviteLink.link;
|
||||
const inviteId = link.split('/').pop()!;
|
||||
|
||||
// owner/member see accept status
|
||||
const { getInviteInfo } = await app.gql({
|
||||
query: getInviteInfoQuery,
|
||||
variables: {
|
||||
inviteId,
|
||||
},
|
||||
});
|
||||
t.truthy(getInviteInfo, 'failed to get invite info');
|
||||
t.is(getInviteInfo.status, WorkspaceMemberStatus.Accepted);
|
||||
|
||||
// non-member see null status
|
||||
await app.signup();
|
||||
const { getInviteInfo: getInviteInfo2 } = await app.gql({
|
||||
query: getInviteInfoQuery,
|
||||
variables: {
|
||||
inviteId,
|
||||
},
|
||||
});
|
||||
t.truthy(getInviteInfo2, 'failed to get invite info');
|
||||
t.is(getInviteInfo2.status, null);
|
||||
|
||||
// pending-member see under review status
|
||||
await app.signup();
|
||||
await app.gql({
|
||||
query: acceptInviteByInviteIdMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
inviteId,
|
||||
},
|
||||
});
|
||||
const { getInviteInfo: getInviteInfo3 } = await app.gql({
|
||||
query: getInviteInfoQuery,
|
||||
variables: {
|
||||
inviteId,
|
||||
},
|
||||
});
|
||||
t.truthy(getInviteInfo3, 'failed to get invite info');
|
||||
t.is(getInviteInfo3.status, WorkspaceMemberStatus.UnderReview);
|
||||
});
|
||||
|
||||
e2e(
|
||||
'should accept invitation by link directly if status is pending',
|
||||
async t => {
|
||||
const owner = await app.create(Mockers.User);
|
||||
const member = await app.create(Mockers.User);
|
||||
|
||||
const workspace = await app.create(Mockers.Workspace, {
|
||||
owner: { id: owner.id },
|
||||
});
|
||||
|
||||
await app.login(owner);
|
||||
// create a pending invitation
|
||||
const invite = await app.gql({
|
||||
query: inviteByEmailsMutation,
|
||||
variables: {
|
||||
emails: [member.email],
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
});
|
||||
t.truthy(invite, 'failed to create invitation');
|
||||
|
||||
const { createInviteLink } = await app.gql({
|
||||
query: createInviteLinkMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
expireTime: WorkspaceInviteLinkExpireTime.OneDay,
|
||||
},
|
||||
});
|
||||
t.truthy(createInviteLink, 'failed to create invite link');
|
||||
const link = createInviteLink.link;
|
||||
const inviteLinkId = link.split('/').pop()!;
|
||||
|
||||
// member accept invitation by link
|
||||
await app.login(member);
|
||||
await app.gql({
|
||||
query: acceptInviteByInviteIdMutation,
|
||||
variables: {
|
||||
inviteId: inviteLinkId,
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
});
|
||||
|
||||
const { getInviteInfo } = await app.gql({
|
||||
query: getInviteInfoQuery,
|
||||
variables: {
|
||||
inviteId: invite.inviteMembers[0].inviteId!,
|
||||
},
|
||||
});
|
||||
t.is(getInviteInfo.status, WorkspaceMemberStatus.Accepted);
|
||||
}
|
||||
);
|
||||
|
||||
e2e(
|
||||
'should invite by link and send review request notification below quota limit',
|
||||
async t => {
|
||||
const { owner, workspace } = await createTeamWorkspace();
|
||||
|
||||
await app.login(owner);
|
||||
const { createInviteLink } = await app.gql({
|
||||
query: createInviteLinkMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
expireTime: WorkspaceInviteLinkExpireTime.OneDay,
|
||||
},
|
||||
});
|
||||
t.truthy(createInviteLink, 'failed to create invite link');
|
||||
const link = createInviteLink.link;
|
||||
const inviteId = link.split('/').pop()!;
|
||||
|
||||
// accept invite by link
|
||||
await app.signup();
|
||||
const result = await app.gql({
|
||||
query: acceptInviteByInviteIdMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
inviteId,
|
||||
},
|
||||
});
|
||||
t.truthy(result, 'failed to accept invite');
|
||||
const notification = app.queue.last(
|
||||
'notification.sendInvitationReviewRequest'
|
||||
);
|
||||
t.is(notification.payload.reviewerId, owner.id);
|
||||
t.truthy(notification.payload.inviteId);
|
||||
}
|
||||
);
|
||||
|
||||
e2e(
|
||||
'should invite by link and send review request notification over quota limit',
|
||||
async t => {
|
||||
const { owner, workspace } = await createTeamWorkspace(1);
|
||||
|
||||
await app.login(owner);
|
||||
const { createInviteLink } = await app.gql({
|
||||
query: createInviteLinkMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
expireTime: WorkspaceInviteLinkExpireTime.OneDay,
|
||||
},
|
||||
});
|
||||
t.truthy(createInviteLink, 'failed to create invite link');
|
||||
const link = createInviteLink.link;
|
||||
const inviteId = link.split('/').pop()!;
|
||||
|
||||
// accept invite by link
|
||||
await app.signup();
|
||||
const result = await app.gql({
|
||||
query: acceptInviteByInviteIdMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
inviteId,
|
||||
},
|
||||
});
|
||||
t.truthy(result, 'failed to accept invite');
|
||||
const notification = app.queue.last(
|
||||
'notification.sendInvitationReviewRequest'
|
||||
);
|
||||
t.is(notification.payload.reviewerId, owner.id);
|
||||
t.truthy(notification.payload.inviteId);
|
||||
}
|
||||
);
|
||||
|
||||
e2e(
|
||||
'should search members by name and email support case insensitive',
|
||||
async t => {
|
||||
|
||||
@@ -1,146 +1,167 @@
|
||||
import {
|
||||
acceptInviteByInviteIdMutation,
|
||||
createInviteLinkMutation,
|
||||
getInviteInfoQuery,
|
||||
inviteByEmailMutation,
|
||||
WorkspaceInviteLinkExpireTime,
|
||||
inviteByEmailsMutation,
|
||||
WorkspaceMemberStatus,
|
||||
} from '@affine/graphql';
|
||||
|
||||
import { WorkspaceRole } from '../../../models';
|
||||
import { Mockers } from '../../mocks';
|
||||
import { app, e2e } from '../test';
|
||||
|
||||
async function createTeamWorkspace(quantity = 10) {
|
||||
const createTeamWorkspace = async (memberLimit = 3) => {
|
||||
const owner = await app.create(Mockers.User);
|
||||
|
||||
const workspace = await app.create(Mockers.Workspace, {
|
||||
owner: { id: owner.id },
|
||||
owner: {
|
||||
id: owner.id,
|
||||
},
|
||||
});
|
||||
await app.create(Mockers.TeamWorkspace, {
|
||||
id: workspace.id,
|
||||
quantity,
|
||||
quantity: memberLimit,
|
||||
});
|
||||
|
||||
const writer = await app.create(Mockers.User);
|
||||
await app.create(Mockers.WorkspaceUser, {
|
||||
userId: writer.id,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
const admin = await app.create(Mockers.User);
|
||||
await app.create(Mockers.WorkspaceUser, {
|
||||
userId: admin.id,
|
||||
workspaceId: workspace.id,
|
||||
type: WorkspaceRole.Admin,
|
||||
});
|
||||
|
||||
const external = await app.create(Mockers.User);
|
||||
|
||||
return {
|
||||
owner,
|
||||
workspace,
|
||||
owner,
|
||||
admin,
|
||||
writer,
|
||||
external,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
e2e(
|
||||
'should invite by link and send review request notification below quota limit',
|
||||
async t => {
|
||||
const { owner, workspace } = await createTeamWorkspace();
|
||||
const getInvitationInfo = async (inviteId: string) => {
|
||||
const result = await app.gql({
|
||||
query: getInviteInfoQuery,
|
||||
variables: {
|
||||
inviteId,
|
||||
},
|
||||
});
|
||||
return result.getInviteInfo;
|
||||
};
|
||||
|
||||
await app.login(owner);
|
||||
const { createInviteLink } = await app.gql({
|
||||
query: createInviteLinkMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
expireTime: WorkspaceInviteLinkExpireTime.OneDay,
|
||||
},
|
||||
});
|
||||
t.truthy(createInviteLink, 'failed to create invite link');
|
||||
const link = createInviteLink.link;
|
||||
const inviteId = link.split('/').pop()!;
|
||||
e2e('should set new invited users to AllocatingSeat', async t => {
|
||||
const { owner, workspace } = await createTeamWorkspace();
|
||||
await app.login(owner);
|
||||
|
||||
// accept invite by link
|
||||
await app.signup();
|
||||
const result = await app.gql({
|
||||
query: acceptInviteByInviteIdMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
inviteId,
|
||||
},
|
||||
});
|
||||
t.truthy(result, 'failed to accept invite');
|
||||
const notification = app.queue.last(
|
||||
'notification.sendInvitationReviewRequest'
|
||||
);
|
||||
t.is(notification.payload.reviewerId, owner.id);
|
||||
t.truthy(notification.payload.inviteId);
|
||||
}
|
||||
);
|
||||
const u1 = await app.createUser();
|
||||
|
||||
e2e(
|
||||
'should invite by link and send review request notification over quota limit',
|
||||
async t => {
|
||||
const { owner, workspace } = await createTeamWorkspace(1);
|
||||
const result = await app.gql({
|
||||
query: inviteByEmailsMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
emails: [u1.email],
|
||||
},
|
||||
});
|
||||
|
||||
await app.login(owner);
|
||||
const { createInviteLink } = await app.gql({
|
||||
query: createInviteLinkMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
expireTime: WorkspaceInviteLinkExpireTime.OneDay,
|
||||
},
|
||||
});
|
||||
t.truthy(createInviteLink, 'failed to create invite link');
|
||||
const link = createInviteLink.link;
|
||||
const inviteId = link.split('/').pop()!;
|
||||
t.not(result.inviteMembers[0].inviteId, null);
|
||||
|
||||
// accept invite by link
|
||||
await app.signup();
|
||||
const result = await app.gql({
|
||||
query: acceptInviteByInviteIdMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
inviteId,
|
||||
},
|
||||
});
|
||||
t.truthy(result, 'failed to accept invite');
|
||||
const notification = app.queue.last(
|
||||
'notification.sendInvitationReviewRequest'
|
||||
);
|
||||
t.is(notification.payload.reviewerId, owner.id);
|
||||
t.truthy(notification.payload.inviteId);
|
||||
}
|
||||
);
|
||||
const invitationInfo = await getInvitationInfo(
|
||||
result.inviteMembers[0].inviteId!
|
||||
);
|
||||
t.is(invitationInfo.status, WorkspaceMemberStatus.AllocatingSeat);
|
||||
});
|
||||
|
||||
e2e(
|
||||
'should accept invitation by link directly if status is pending on team workspace',
|
||||
async t => {
|
||||
const { owner, workspace } = await createTeamWorkspace(2);
|
||||
const member = await app.create(Mockers.User);
|
||||
e2e('should allocate seats', async t => {
|
||||
const { owner, workspace } = await createTeamWorkspace();
|
||||
await app.login(owner);
|
||||
|
||||
await app.login(owner);
|
||||
// create a pending invitation
|
||||
const invite = await app.gql({
|
||||
query: inviteByEmailMutation,
|
||||
variables: {
|
||||
email: member.email,
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
});
|
||||
t.truthy(invite, 'failed to create invitation');
|
||||
const u1 = await app.createUser();
|
||||
await app.create(Mockers.WorkspaceUser, {
|
||||
userId: u1.id,
|
||||
workspaceId: workspace.id,
|
||||
status: WorkspaceMemberStatus.AllocatingSeat,
|
||||
source: 'Email',
|
||||
});
|
||||
|
||||
const { createInviteLink } = await app.gql({
|
||||
query: createInviteLinkMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
expireTime: WorkspaceInviteLinkExpireTime.OneDay,
|
||||
},
|
||||
});
|
||||
t.truthy(createInviteLink, 'failed to create invite link');
|
||||
const link = createInviteLink.link;
|
||||
const inviteLinkId = link.split('/').pop()!;
|
||||
const u2 = await app.createUser();
|
||||
await app.create(Mockers.WorkspaceUser, {
|
||||
userId: u2.id,
|
||||
workspaceId: workspace.id,
|
||||
status: WorkspaceMemberStatus.AllocatingSeat,
|
||||
source: 'Link',
|
||||
});
|
||||
|
||||
// member accept invitation by link
|
||||
await app.login(member);
|
||||
await app.gql({
|
||||
query: acceptInviteByInviteIdMutation,
|
||||
variables: {
|
||||
inviteId: inviteLinkId,
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
});
|
||||
await app.eventBus.emitAsync('workspace.members.allocateSeats', {
|
||||
workspaceId: workspace.id,
|
||||
quantity: 5,
|
||||
});
|
||||
|
||||
const { getInviteInfo } = await app.gql({
|
||||
query: getInviteInfoQuery,
|
||||
variables: {
|
||||
inviteId: invite.invite,
|
||||
},
|
||||
});
|
||||
t.is(getInviteInfo.status, WorkspaceMemberStatus.Accepted);
|
||||
}
|
||||
);
|
||||
const [members] = await app.models.workspaceUser.paginate(workspace.id, {
|
||||
first: 10,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
t.is(
|
||||
members.find(m => m.user.id === u1.id)?.status,
|
||||
WorkspaceMemberStatus.Pending
|
||||
);
|
||||
t.is(
|
||||
members.find(m => m.user.id === u2.id)?.status,
|
||||
WorkspaceMemberStatus.Accepted
|
||||
);
|
||||
|
||||
t.is(app.queue.count('notification.sendInvitation'), 1);
|
||||
});
|
||||
|
||||
e2e('should set all rests to NeedMoreSeat', async t => {
|
||||
const { owner, workspace } = await createTeamWorkspace();
|
||||
await app.login(owner);
|
||||
|
||||
const u1 = await app.createUser();
|
||||
await app.create(Mockers.WorkspaceUser, {
|
||||
userId: u1.id,
|
||||
workspaceId: workspace.id,
|
||||
status: WorkspaceMemberStatus.AllocatingSeat,
|
||||
source: 'Email',
|
||||
});
|
||||
|
||||
const u2 = await app.createUser();
|
||||
await app.create(Mockers.WorkspaceUser, {
|
||||
userId: u2.id,
|
||||
workspaceId: workspace.id,
|
||||
status: WorkspaceMemberStatus.AllocatingSeat,
|
||||
source: 'Email',
|
||||
});
|
||||
|
||||
const u3 = await app.createUser();
|
||||
await app.create(Mockers.WorkspaceUser, {
|
||||
userId: u3.id,
|
||||
workspaceId: workspace.id,
|
||||
status: WorkspaceMemberStatus.AllocatingSeat,
|
||||
source: 'Link',
|
||||
});
|
||||
|
||||
await app.eventBus.emitAsync('workspace.members.allocateSeats', {
|
||||
workspaceId: workspace.id,
|
||||
quantity: 4,
|
||||
});
|
||||
|
||||
const [members] = await app.models.workspaceUser.paginate(workspace.id, {
|
||||
first: 10,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
t.is(
|
||||
members.find(m => m.user.id === u2.id)?.status,
|
||||
WorkspaceMemberStatus.NeedMoreSeat
|
||||
);
|
||||
t.is(
|
||||
members.find(m => m.user.id === u3.id)?.status,
|
||||
WorkspaceMemberStatus.NeedMoreSeat
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user