Files
speckle-server/packages/server/modules/workspaces/tests/integration/workspaces.graph.spec.ts
T
Kristaps Fabians Geikins e3d3c1446b feat: enable domain discoverability on workspace creation (#4235)
* frontend changes implemented

* WIP BE

* backend seems to work

* CR fixes
2025-04-07 12:34:45 +03:00

1375 lines
45 KiB
TypeScript

import { expect } from 'chai'
import cryptoRandomString from 'crypto-random-string'
import {
createTestContext,
testApolloServer,
TestApolloServer
} from '@/test/graphqlHelper'
import {
BasicTestUser,
createAuthTokenForUser,
createTestUser,
createTestUsers,
login
} from '@/test/authHelper'
import { Roles, wait, WorkspaceRoles } from '@speckle/shared'
import {
CreateProjectInviteDocument,
CreateWorkspaceDocument,
DeleteWorkspaceDocument,
GetActiveUserWorkspacesDocument,
GetWorkspaceDocument,
GetWorkspaceBySlugDocument,
GetWorkspaceTeamDocument,
UpdateWorkspaceDocument,
UpdateWorkspaceRoleDocument,
ActiveUserLeaveWorkspaceDocument,
GetWorkspaceWithProjectsDocument,
AddWorkspaceDomainDocument,
DeleteWorkspaceDomainDocument,
CreateWorkspaceProjectDocument,
DismissWorkspaceDocument,
GetActiveUserDiscoverableWorkspacesDocument,
GetWorkspaceWithMembersByRoleDocument
} from '@/test/graphql/generated/graphql'
import { beforeEachContext } from '@/test/hooks'
import { AllScopes } from '@/modules/core/helpers/mainConstants'
import {
assignToWorkspace,
BasicTestWorkspace,
createTestWorkspace,
createWorkspaceInviteDirectly
} from '@/modules/workspaces/tests/helpers/creation'
import { BasicTestCommit, createTestCommit } from '@/test/speckle-helpers/commitHelper'
import { BasicTestStream, createTestStream } from '@/test/speckle-helpers/streamHelper'
import { shuffle } from 'lodash'
import knex, { db } from '@/db/knex'
import {
createRandomPassword,
createRandomEmail,
createRandomString
} from '@/modules/core/helpers/testHelpers'
import {
getWorkspaceFactory,
getWorkspaceRoleForUserFactory
} from '@/modules/workspaces/repositories/workspaces'
import { grantStreamPermissionsFactory } from '@/modules/core/repositories/streams'
import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import {
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory,
findEmailFactory
} from '@/modules/core/repositories/userEmails'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { getUserFactory } from '@/modules/core/repositories/users'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { sendEmail } from '@/modules/emails/services/sending'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { itEach } from '@/test/assertionHelper'
import { assignWorkspaceSeatFactory } from '@/modules/workspaces/services/workspaceSeat'
import { createWorkspaceSeatFactory } from '@/modules/gatekeeper/repositories/workspaceSeat'
import { WorkspaceSeatType } from '@/modules/gatekeeper/domain/billing'
const grantStreamPermissions = grantStreamPermissionsFactory({ db })
const validateAndCreateUserEmail = validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail: findEmailFactory({ db }),
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification: requestNewEmailVerificationFactory({
findEmail: findEmailFactory({ db }),
getUser: getUserFactory({ db }),
getServerInfo: getServerInfoFactory({ db }),
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
sendEmail,
renderEmail
})
})
const { FF_GATEKEEPER_FORCE_FREE_PLAN } = getFeatureFlags()
describe('Workspaces GQL CRUD', () => {
let apollo: TestApolloServer
const testAdminUser: BasicTestUser = {
id: '',
name: 'John Speckle',
email: 'john-speckle@example.org',
role: Roles.Server.Admin,
verified: true
}
const testMemberUser: BasicTestUser = {
id: '',
name: 'Alice speckle',
email: 'alice-speckle@example.org',
role: Roles.Server.User
}
before(async () => {
await beforeEachContext()
await createTestUsers([testAdminUser, testMemberUser])
const token = await createAuthTokenForUser(testAdminUser.id, AllScopes)
apollo = await testApolloServer({
context: await createTestContext({
auth: true,
userId: testAdminUser.id,
token,
role: testAdminUser.role,
scopes: AllScopes
})
})
})
describe('retrieval operations', () => {
let apollo: TestApolloServer
const workspace: BasicTestWorkspace = {
id: '',
ownerId: '',
name: 'Workspace A',
slug: cryptoRandomString({ length: 10 })
}
const testMemberUser: BasicTestUser = {
id: '',
name: 'Jimmy Speckle',
email: 'jimmy-speckle@example.org'
}
const testMemberUser2: BasicTestUser = {
id: '',
name: 'Some Dude',
email: 'some-dude@example.org'
}
before(async () => {
await createTestUsers([testMemberUser, testMemberUser2])
await createTestWorkspace(workspace, testMemberUser)
await assignToWorkspace(workspace, testMemberUser2, Roles.Workspace.Member)
apollo = await testApolloServer({
authUserId: testMemberUser.id
})
})
describe('query workspace by slug', () => {
it('should return a workspace that exists', async () => {
const res = await apollo.execute(GetWorkspaceBySlugDocument, {
workspaceSlug: workspace.slug
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.workspaceBySlug.id).to.equal(workspace.id)
})
it('throw a not found error if the workspace does not exist', async () => {
const res = await apollo.execute(GetWorkspaceBySlugDocument, {
workspaceSlug: cryptoRandomString({ length: 6 })
})
expect(res).to.haveGraphQLErrors('not found')
})
})
describe('query workspace', () => {
it('should return a workspace that exists', async () => {
const res = await apollo.execute(GetWorkspaceDocument, {
workspaceId: workspace.id
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.workspace).to.exist
})
it('throw a not found error if the workspace does not exist', async () => {
const res = await apollo.execute(GetWorkspaceDocument, {
workspaceId: cryptoRandomString({ length: 6 })
})
expect(res).to.haveGraphQLErrors('not found')
})
})
describe('query workspace.team', () => {
let largeWorkspaceApollo: TestApolloServer
const largeWorkspace: BasicTestWorkspace = {
id: '',
ownerId: '',
name: 'My Large Workspace',
slug: cryptoRandomString({ length: 10 }),
description: 'A workspace with many users and roles to test pagination.'
}
const largeWorkspaceAdmin: BasicTestUser = {
id: '',
name: 'John A Speckle',
email: 'large-workspace-user-a@example.org'
}
before(async () => {
await createTestUser(largeWorkspaceAdmin)
await createTestWorkspace(largeWorkspace, largeWorkspaceAdmin)
largeWorkspaceApollo = await testApolloServer({
authUserId: largeWorkspaceAdmin.id
})
const workspaceMembers: [BasicTestUser, WorkspaceRoles][] = [
[
{
id: '',
name: 'John B Speckle',
email: 'large-workspace-user-b@example.org'
},
'workspace:admin'
],
[
{
id: '',
name: 'John C Speckle',
email: 'large-workspace-user-c@example.org'
},
'workspace:member'
],
[
{
id: '',
name: 'John D Speckle',
email: 'large-workspace-user-d@example.org'
},
'workspace:member'
],
[
{
id: '',
name: 'John F Speckle',
email: 'large-workspace-user-f-1@example.org'
},
'workspace:guest'
],
[
{
id: '',
name: 'John F Speckle',
email: 'large-workspace-user-f-2@example.org'
},
'workspace:guest'
]
]
for (const [user, role] of workspaceMembers) {
await createTestUser(user)
await assignToWorkspace(largeWorkspace, user, role)
// Overly-careful guarantee that `createdAt` values are different
await wait(1)
}
for (const [user, role] of shuffle(workspaceMembers)) {
// Simulate future changes to user roles
await assignToWorkspace(largeWorkspace, user, role)
await wait(1)
}
})
it('should return workspace members', async () => {
const res = await largeWorkspaceApollo.execute(GetWorkspaceTeamDocument, {
workspaceId: largeWorkspace.id,
limit: 25
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.workspace.team.items.length).to.equal(6)
})
it('should return workspace members in the correct order', async () => {
const res = await largeWorkspaceApollo.execute(GetWorkspaceTeamDocument, {
workspaceId: largeWorkspace.id
})
const memberNames = res.data?.workspace.team.items.map((user) => user.user.name)
const expectedMemberNames = [
'John F Speckle',
'John F Speckle',
'John D Speckle',
'John C Speckle',
'John B Speckle',
'John A Speckle'
]
expect(res).to.not.haveGraphQLErrors()
expect(memberNames).to.deep.equal(expectedMemberNames)
})
it('should respect search filters', async () => {
const res = await largeWorkspaceApollo.execute(GetWorkspaceTeamDocument, {
workspaceId: largeWorkspace.id,
filter: {
search: 'John C'
}
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.workspace.team.items.length).to.equal(1)
expect(res.data?.workspace.team.items[0].user.name).to.equal('John C Speckle')
})
it('should respect role filters with one value', async () => {
const res = await largeWorkspaceApollo.execute(GetWorkspaceTeamDocument, {
workspaceId: largeWorkspace.id,
filter: {
roles: ['workspace:member']
}
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.workspace.team.items.length).to.equal(2)
})
it('should respect role filters with multiple values', async () => {
const res = await largeWorkspaceApollo.execute(GetWorkspaceTeamDocument, {
workspaceId: largeWorkspace.id,
filter: {
roles: ['workspace:admin', 'workspace:member']
}
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.workspace.team.items.length).to.equal(4)
})
it('should respect search limits', async () => {
const res = await largeWorkspaceApollo.execute(GetWorkspaceTeamDocument, {
workspaceId: largeWorkspace.id,
limit: 1
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.workspace.team.items.length).to.equal(1)
expect(res.data?.workspace.team.cursor).to.exist
})
it('should respect seatType filter', async () => {
const admin = await createTestUser({
id: createRandomString(),
name: createRandomString(),
email: createRandomEmail(),
role: Roles.Server.User,
verified: true
})
const workspace = {
id: createRandomString(),
ownerId: admin.id,
name: createRandomString(),
slug: cryptoRandomString({ length: 10 })
}
await createTestWorkspace(workspace, admin)
const otherWorkspace = {
id: createRandomString(),
ownerId: admin.id,
name: createRandomString(),
slug: cryptoRandomString({ length: 10 })
}
await createTestWorkspace(otherWorkspace, admin)
const session = await login(admin)
const memberEditor = {
id: createRandomString(),
name: createRandomString(),
email: createRandomEmail()
}
await createTestUser(memberEditor)
await assignToWorkspace(workspace, memberEditor, 'workspace:member')
await assignWorkspaceSeatFactory({
createWorkspaceSeat: createWorkspaceSeatFactory({ db }),
getWorkspaceRoleForUser: getWorkspaceRoleForUserFactory({ db }),
eventEmit: async () => {}
})({
workspaceId: workspace.id,
userId: memberEditor.id,
type: 'editor',
assignedByUserId: admin.id
})
// Assign the same user editor to another workspace
await assignToWorkspace(otherWorkspace, memberEditor, 'workspace:member')
await assignWorkspaceSeatFactory({
createWorkspaceSeat: createWorkspaceSeatFactory({ db }),
getWorkspaceRoleForUser: getWorkspaceRoleForUserFactory({ db }),
eventEmit: async () => {}
})({
workspaceId: otherWorkspace.id,
userId: memberEditor.id,
type: 'editor',
assignedByUserId: admin.id
})
const memberViewer = {
id: createRandomString(),
name: createRandomString(),
email: createRandomEmail()
}
await createTestUser(memberViewer)
await assignToWorkspace(workspace, memberViewer, 'workspace:member')
await assignWorkspaceSeatFactory({
createWorkspaceSeat: createWorkspaceSeatFactory({ db }),
getWorkspaceRoleForUser: getWorkspaceRoleForUserFactory({ db }),
eventEmit: async () => {}
})({
workspaceId: workspace.id,
userId: memberViewer.id,
type: 'viewer',
assignedByUserId: admin.id
})
const res = await session.execute(GetWorkspaceTeamDocument, {
workspaceId: workspace.id,
filter: {
seatType: 'editor'
}
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.workspace.team.items.length).to.equal(2)
expect(res.data?.workspace.team.items.length).to.equal(2)
const team = res.data?.workspace.team.items
expect(team?.[1].user.name).to.eq(admin.name)
expect(team?.[0].user.name).to.eq(memberEditor.name)
})
it('should respect team pagination', async () => {
const resA = await largeWorkspaceApollo.execute(GetWorkspaceTeamDocument, {
workspaceId: largeWorkspace.id,
limit: 2
})
const resB = await largeWorkspaceApollo.execute(GetWorkspaceTeamDocument, {
workspaceId: largeWorkspace.id,
limit: 10,
cursor: resA.data?.workspace.team.cursor
})
expect(resA).to.not.haveGraphQLErrors()
expect(resA.data?.workspace.team.items.length).to.equal(2)
expect(
resA.data?.workspace.team.items.every(
(user) => user.role === 'workspace:admin'
)
)
expect(resA.data?.workspace.team.cursor).to.exist
expect(resB).to.not.haveGraphQLErrors()
expect(resB.data?.workspace.team.items.length).to.equal(4)
expect(resB.data?.workspace.team.cursor).to.be.null
})
it('should return correct total count', async () => {
const res = await largeWorkspaceApollo.execute(GetWorkspaceTeamDocument, {
workspaceId: largeWorkspace.id
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.workspace.team.totalCount).to.equal(6)
})
it('should return correct total count while paginating', async () => {
const res = await largeWorkspaceApollo.execute(GetWorkspaceTeamDocument, {
workspaceId: largeWorkspace.id,
limit: 1
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.workspace.team.totalCount).to.equal(6)
})
it('should return workspace team projectRoles', async () => {
const createRes = await apollo.execute(CreateWorkspaceDocument, {
input: { name: createRandomString() }
})
expect(createRes).to.not.haveGraphQLErrors()
const workspaceId = createRes.data!.workspaceMutations.create.id
const workspace = (await getWorkspaceFactory({ db })({
workspaceId
})) as unknown as BasicTestWorkspace
const member = {
id: createRandomString(),
name: createRandomPassword(),
email: createRandomEmail()
}
const guest = {
id: createRandomString(),
name: createRandomPassword(),
email: createRandomEmail()
}
await Promise.all([createTestUser(member), createTestUser(guest)])
await Promise.all([
assignToWorkspace(workspace, member, Roles.Workspace.Member),
assignToWorkspace(workspace, guest, Roles.Workspace.Guest)
])
const resProject1 = await apollo.execute(CreateWorkspaceProjectDocument, {
input: {
name: createRandomPassword(),
workspaceId
}
})
expect(resProject1).to.not.haveGraphQLErrors()
const project1Id = resProject1.data!.workspaceMutations.projects.create.id
const project1Name = resProject1.data!.workspaceMutations.projects.create.name
const resProject2 = await apollo.execute(CreateWorkspaceProjectDocument, {
input: {
name: createRandomPassword(),
workspaceId
}
})
expect(resProject2).to.not.haveGraphQLErrors()
const project2Id = resProject2.data!.workspaceMutations.projects.create.id
const project2Name = resProject2.data!.workspaceMutations.projects.create.name
await Promise.all([
grantStreamPermissions({
streamId: project1Id,
userId: member.id,
role: Roles.Stream.Contributor
}),
grantStreamPermissions({
streamId: project1Id,
userId: guest.id,
role: Roles.Stream.Reviewer
}),
grantStreamPermissions({
streamId: project2Id,
userId: guest.id,
role: Roles.Stream.Contributor
})
])
const res = await apollo.execute(GetWorkspaceTeamDocument, {
workspaceId
})
expect(res).to.not.haveGraphQLErrors()
const items = res.data?.workspace!.team?.items ?? []
expect(items).to.have.length(3)
const adminRoles = items.find(
(item) => item.role === Roles.Workspace.Admin
)?.projectRoles
expect(adminRoles).to.have.deep.members([
{
role: Roles.Stream.Owner,
project: {
id: project1Id,
name: project1Name
}
},
{
role: Roles.Stream.Owner,
project: {
id: project2Id,
name: project2Name
}
}
])
const memberRoles = items.find(
(item) => item.role === Roles.Workspace.Member
)?.projectRoles
expect(memberRoles).to.have.deep.members([
{
role: Roles.Stream.Contributor,
project: {
id: project1Id,
name: project1Name
}
},
// No longer auto-assigned in new plan world (until we rework auth & queries)
...(FF_GATEKEEPER_FORCE_FREE_PLAN
? []
: [
{
role: Roles.Stream.Reviewer,
project: {
id: project2Id,
name: project2Name
}
}
])
])
const guestRoles = items.find(
(item) => item.role === Roles.Workspace.Guest
)?.projectRoles
expect(guestRoles).to.have.deep.members([
{
role: Roles.Stream.Reviewer,
project: {
id: project1Id,
name: project1Name
}
},
{
role: Roles.Stream.Contributor,
project: {
id: project2Id,
name: project2Name
}
}
])
})
})
describe('LimitedWorkspace.team', () => {
it('should return a limited workspace team', async () => {
const email = createRandomEmail()
const domain = email.split('@')[1]
const user = await createTestUser({
id: createRandomString(),
name: createRandomString(),
email,
role: Roles.Server.Admin,
verified: true
})
const session = await login(user)
const workspaceAdmin = await createTestUser({
id: createRandomString(),
name: 'John Speckle',
email: createRandomEmail(),
role: Roles.Server.Admin,
verified: true,
avatar: 'data:image/png;base64,foobar'
})
await createTestWorkspace(
{
id: createRandomString(),
ownerId: workspaceAdmin.id,
name: 'Workspace A',
slug: cryptoRandomString({ length: 10 }),
discoverabilityEnabled: true
},
workspaceAdmin,
{
domain
}
)
await createTestWorkspace(
{
id: createRandomString(),
ownerId: workspaceAdmin.id,
name: 'Workspace B',
slug: cryptoRandomString({ length: 10 }),
discoverabilityEnabled: true
},
workspaceAdmin,
{
domain
}
)
// Workspace without domain should not be returned
await createTestWorkspace(
{
id: createRandomString(),
ownerId: workspaceAdmin.id,
name: 'Workspace C',
slug: cryptoRandomString({ length: 10 })
},
workspaceAdmin
)
const res = await session.execute(
GetActiveUserDiscoverableWorkspacesDocument,
{}
)
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.activeUser?.discoverableWorkspaces).to.have.length(2)
const discoverableWorkspaces = res.data?.activeUser?.discoverableWorkspaces
expect(discoverableWorkspaces?.[0].team?.items).to.have.length(1)
expect(discoverableWorkspaces?.[0].team?.items[0].avatar).to.eq(
workspaceAdmin.avatar
)
expect(discoverableWorkspaces?.[1].team?.items).to.have.length(1)
expect(discoverableWorkspaces?.[1].team?.items[0].avatar).to.eq(
workspaceAdmin.avatar
)
})
})
describe('query activeUser.workspaces', () => {
it('should return all workspaces for a user', async () => {
const testUser: BasicTestUser = {
id: '',
name: 'John Speckle',
email: 'foobar@example.org',
role: Roles.Server.Admin,
verified: true
}
await createTestUser(testUser)
const testApollo: TestApolloServer = await testApolloServer({
context: await createTestContext({
auth: true,
userId: testUser.id,
token: '',
role: testUser.role,
scopes: AllScopes
})
})
const workspace1: BasicTestWorkspace = {
id: '',
ownerId: '',
name: 'Workspace A',
slug: cryptoRandomString({ length: 10 })
}
const workspace2: BasicTestWorkspace = {
id: '',
ownerId: '',
name: 'Workspace A',
slug: cryptoRandomString({ length: 10 })
}
const workspace3: BasicTestWorkspace = {
id: '',
ownerId: '',
name: 'Workspace A',
slug: cryptoRandomString({ length: 10 })
}
await createTestWorkspace(workspace1, testUser)
await createTestWorkspace(workspace2, testUser)
await createTestWorkspace(workspace3, testUser)
const res = await testApollo.execute(GetActiveUserWorkspacesDocument, {})
expect(res).to.not.haveGraphQLErrors()
// TODO: this test depends on the previous tests
expect(res.data?.activeUser?.workspaces?.items?.length).to.equal(3)
})
})
describe('query workspace.projects', () => {
it('should return all projects for a user in the workspace', async () => {
const workspace = {
id: '',
name: 'test ws',
slug: cryptoRandomString({ length: 10 }),
ownerId: ''
}
await createTestWorkspace(workspace, testMemberUser)
const user = {
id: createRandomPassword(),
email: createRandomEmail(),
name: createRandomPassword()
}
const resProject1 = await apollo.execute(CreateWorkspaceProjectDocument, {
input: {
name: createRandomPassword(),
workspaceId: workspace.id
}
})
expect(resProject1).to.not.haveGraphQLErrors()
const res = await apollo.execute(GetWorkspaceWithProjectsDocument, {
workspaceId: workspace.id
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.workspace?.projects.items?.length).to.equal(1)
expect(res.data?.workspace?.projects.totalCount).to.equal(1)
// Test Guest user
await createTestUser(user)
const sessionGuest = await testApolloServer({
authUserId: user.id
})
await assignToWorkspace(workspace, user, Roles.Workspace.Guest)
const res2 = await sessionGuest.execute(GetWorkspaceWithProjectsDocument, {
workspaceId: workspace.id
})
expect(res2).to.not.haveGraphQLErrors()
expect(res2.data?.workspace?.projects.items?.length).to.equal(0)
expect(res2.data?.workspace?.projects.totalCount).to.equal(0)
})
})
describe('workspace.membersByRole', () => {
it('should return admins and members and guests in the workspace', async () => {
const user = await createTestUser({
name: createRandomString(),
email: createRandomEmail(),
role: Roles.Server.Admin,
verified: true
})
const workspace = {
id: createRandomString(),
name: createRandomString(),
slug: cryptoRandomString({ length: 10 }),
ownerId: user.id
}
await createTestWorkspace(workspace, user, {
addPlan: { name: 'pro', status: 'valid' }
})
const guest1 = await createTestUser({
name: createRandomString(),
email: createRandomEmail(),
role: Roles.Server.User,
verified: true
})
await assignToWorkspace(
workspace,
guest1,
Roles.Workspace.Guest,
WorkspaceSeatType.Viewer
)
const guest2 = await createTestUser({
name: createRandomString(),
email: createRandomEmail(),
role: Roles.Server.User,
verified: true
})
await assignToWorkspace(
workspace,
guest2,
Roles.Workspace.Guest,
WorkspaceSeatType.Viewer
)
const member1 = await createTestUser({
name: createRandomString(),
email: createRandomEmail(),
role: Roles.Server.User,
verified: true
})
await assignToWorkspace(
workspace,
member1,
Roles.Workspace.Member,
WorkspaceSeatType.Editor
)
const member2 = await createTestUser({
name: createRandomString(),
email: createRandomEmail(),
role: Roles.Server.User,
verified: true
})
await assignToWorkspace(
workspace,
member2,
Roles.Workspace.Member,
WorkspaceSeatType.Editor
)
const member3 = await createTestUser({
name: createRandomString(),
email: createRandomEmail(),
role: Roles.Server.User,
verified: true
})
await assignToWorkspace(
workspace,
member3,
Roles.Workspace.Member,
WorkspaceSeatType.Editor
)
const session = await login(user)
const res = await session.execute(GetWorkspaceWithMembersByRoleDocument, {
workspaceId: workspace.id
})
expect(res).to.not.haveGraphQLErrors()
const roles = res.data?.workspace.teamByRole
expect(roles?.guests?.totalCount).to.eq(2)
expect(roles?.members?.totalCount).to.eq(3)
expect(roles?.admins?.totalCount).to.eq(1)
})
})
})
describe('management operations', () => {
describe('mutation workspaceMutations.create', () => {
it('should create a workspace', async () => {
const workspaceName = cryptoRandomString({ length: 6 })
const workspaceSlug = cryptoRandomString({ length: 10 })
const createRes = await apollo.execute(CreateWorkspaceDocument, {
input: { name: workspaceName, slug: workspaceSlug }
})
const getRes = await apollo.execute(GetWorkspaceDocument, {
workspaceId: createRes.data!.workspaceMutations.create.id
})
expect(createRes).to.not.haveGraphQLErrors()
expect(getRes).to.not.haveGraphQLErrors()
expect(getRes.data?.workspace).to.exist
expect(getRes.data?.workspace?.name).to.equal(workspaceName)
expect(getRes.data?.workspace?.slug).to.equal(workspaceSlug)
})
describe('when attempting to enable domain discoverability', () => {
const guyWithNoVerifiedEmails: BasicTestUser = {
id: '',
name: 'Guy with no verified emails',
email: 'guy-with-no-verified-emails@bozo1.org',
verified: false
}
const guyWithMultipleVerifiedEmails: BasicTestUser = {
id: '',
name: 'Guy with multiple verified emails',
email: 'guy-with-multiple-verified-emails@bozo2.org',
verified: true
}
const guyWithOneVerifiedEmail: BasicTestUser = {
id: '',
name: 'Guy with one verified email',
email: 'guy-with-one-verified-email@bozo3.org',
verified: true
}
const guyWithOneBlockedVerifiedEmail: BasicTestUser = {
id: '',
name: 'Guy with one blocked verified email',
email: 'guy-with-one-blocked-verified-email@gmail.com',
verified: true,
allowPersonalEmail: true
}
const getDomain = (user: BasicTestUser) => user.email.split('@')[1]
before(async () => {
await createTestUsers([
guyWithNoVerifiedEmails,
guyWithMultipleVerifiedEmails,
guyWithOneVerifiedEmail,
guyWithOneBlockedVerifiedEmail
])
await Promise.all([
validateAndCreateUserEmail({
userEmail: {
userId: guyWithMultipleVerifiedEmails.id,
email: 'guy-with-multiple-verified-emails@bozo22.org',
verified: true
}
}),
validateAndCreateUserEmail({
userEmail: {
userId: guyWithMultipleVerifiedEmails.id,
email: 'guy-with-multiple-verified-emails@bozo23.org',
verified: true
}
})
])
})
itEach(
[guyWithOneVerifiedEmail, guyWithMultipleVerifiedEmails],
(user) => `${user.name} can create with enabled domain discoverability`,
async (user) => {
const apollo = await testApolloServer({
authUserId: user.id
})
const createRes = await apollo.execute(CreateWorkspaceDocument, {
input: {
name: `${user.name} Domain Discoverability Workspace`,
slug: cryptoRandomString({ length: 10 }),
enableDomainDiscoverabilityForDomain: getDomain(user)
}
})
expect(createRes).to.not.haveGraphQLErrors()
expect(createRes.data?.workspaceMutations.create.id).to.be.ok
expect(createRes.data!.workspaceMutations.create.discoverabilityEnabled).to
.be.true
}
)
itEach(
[guyWithNoVerifiedEmails, guyWithOneBlockedVerifiedEmail],
(user) => `${user.name} can not create with enabled domain discoverability`,
async (user) => {
const apollo = await testApolloServer({
authUserId: user.id
})
const createRes = await apollo.execute(CreateWorkspaceDocument, {
input: {
name: `${user.name} Domain Discoverability Workspace`,
slug: cryptoRandomString({ length: 10 }),
enableDomainDiscoverabilityForDomain: getDomain(user)
}
})
expect(createRes).to.haveGraphQLErrors()
expect(createRes.data?.workspaceMutations.create).to.not.be.ok
}
)
})
})
describe('mutation workspaceMutations.delete', () => {
const workspace: BasicTestWorkspace = {
id: '',
ownerId: '',
slug: cryptoRandomString({ length: 10 }),
name: 'My Test Workspace'
}
const workspaceProject: BasicTestStream = {
id: '',
ownerId: '',
name: 'My Test Project',
isPublic: false
}
before(async () => {
await createTestWorkspace(workspace, testAdminUser, { addPlan: false })
workspaceProject.workspaceId = workspace.id
await createWorkspaceInviteDirectly(
{
workspaceId: workspace.id,
input: {
userId: testMemberUser.id
}
},
testAdminUser.id
)
await createTestStream(workspaceProject, testAdminUser)
await apollo.execute(CreateProjectInviteDocument, {
projectId: workspaceProject.id,
input: { userId: testMemberUser.id }
})
const testVersion: BasicTestCommit = {
id: cryptoRandomString({ length: 10 }),
streamId: workspaceProject.id,
objectId: '',
authorId: ''
}
await createTestCommit(testVersion, {
owner: testAdminUser,
stream: workspaceProject
})
})
it('should fail to delete a paid workspace', async () => {
const paidWorkspace = {
id: '',
name: 'test ws',
slug: cryptoRandomString({ length: 10 }),
ownerId: ''
}
await createTestWorkspace(paidWorkspace, testAdminUser, { addPlan: true })
const deleteRes = await apollo.execute(DeleteWorkspaceDocument, {
workspaceId: paidWorkspace.id
})
expect(deleteRes).to.haveGraphQLErrors('Workspace has an active paid plan')
})
it('should delete the workspace', async () => {
const deleteRes = await apollo.execute(DeleteWorkspaceDocument, {
workspaceId: workspace.id
})
const getRes = await apollo.execute(GetWorkspaceDocument, {
workspaceId: workspace.id
})
expect(deleteRes).to.not.haveGraphQLErrors()
expect(getRes).to.haveGraphQLErrors('Workspace not found')
})
it('should throw if non-workspace-admin triggers delete', async () => {
const nonPaidWorkspace = {
id: '',
name: 'test ws',
slug: cryptoRandomString({ length: 10 }),
ownerId: ''
}
await createTestWorkspace(nonPaidWorkspace, testAdminUser, { addPlan: false })
const memberApollo: TestApolloServer = await testApolloServer({
context: await createTestContext({
auth: true,
userId: testMemberUser.id,
token: '',
role: testMemberUser.role,
scopes: AllScopes
})
})
const res = await memberApollo.execute(DeleteWorkspaceDocument, {
workspaceId: nonPaidWorkspace.id
})
expect(res).to.haveGraphQLErrors('not authorized')
})
it('should delete all workspace projects', async () => {
const projects = await knex('streams').where({ workspaceId: workspace.id })
expect(projects.length).to.equal(0)
})
it('should delete pending workspace and project invites', async () => {
const invites = await knex('server_invites').where({
inviterId: testAdminUser.id
})
expect(invites.length).to.equal(0)
})
it('should delete all workspace project commits', async () => {
const versions = await knex('stream_commits').where({
streamId: workspaceProject.id
})
expect(versions.length).to.equal(0)
})
})
describe('mutation workspaceMutations.update', () => {
const workspace: BasicTestWorkspace = {
id: '',
slug: '',
ownerId: '',
name: cryptoRandomString({ length: 6 }),
description: cryptoRandomString({ length: 12 })
}
beforeEach(async () => {
// we want a new workspace for each test
workspace.id = ''
workspace.slug = ''
await createTestWorkspace(workspace, testAdminUser)
})
it('should update a workspace', async () => {
const workspaceName = cryptoRandomString({ length: 6 })
const updateRes = await apollo.execute(UpdateWorkspaceDocument, {
input: {
id: workspace.id,
name: workspaceName
}
})
const { data } = await apollo.execute(GetWorkspaceDocument, {
workspaceId: workspace.id
})
expect(updateRes).to.not.haveGraphQLErrors()
expect(data?.workspace.name).to.equal(workspaceName)
})
it('should not allow workspace name to be empty', async () => {
const updateRes = await apollo.execute(UpdateWorkspaceDocument, {
input: {
id: workspace.id,
name: ''
}
})
const { data } = await apollo.execute(GetWorkspaceDocument, {
workspaceId: workspace.id
})
expect(updateRes).to.not.haveGraphQLErrors()
expect(data?.workspace.name).to.equal(workspace.name)
})
it('should allow workspace description to be empty', async () => {
const updateRes = await apollo.execute(UpdateWorkspaceDocument, {
input: {
id: workspace.id,
description: ''
}
})
const { data } = await apollo.execute(GetWorkspaceDocument, {
workspaceId: workspace.id
})
expect(updateRes).to.not.haveGraphQLErrors()
expect(data?.workspace.description).to.equal('')
})
it('should limit workspace descriptions to 512 characters', async () => {
const updateRes = await apollo.execute(UpdateWorkspaceDocument, {
input: {
id: workspace.id,
description: 'especkle'.repeat(512)
}
})
expect(updateRes).to.haveGraphQLErrors('too long')
})
})
describe('mutation workspaceMutations.dismiss', () => {
it('should return an error if workspace does not exists', async () => {
const res = await apollo.execute(DismissWorkspaceDocument, {
input: {
workspaceId: cryptoRandomString({ length: 6 })
}
})
expect(res).to.haveGraphQLErrors(WorkspaceNotFoundError.defaultMessage)
})
it('should dismiss a workspace', async () => {
const workspace: BasicTestWorkspace = {
id: '',
slug: '',
ownerId: '',
name: cryptoRandomString({ length: 6 }),
description: cryptoRandomString({ length: 12 })
}
await createTestWorkspace(workspace, testAdminUser)
const dismissRes = await apollo.execute(DismissWorkspaceDocument, {
input: {
workspaceId: workspace.id
}
})
expect(dismissRes).to.not.haveGraphQLErrors()
expect(dismissRes?.data?.workspaceMutations.dismiss).to.equal(true)
})
})
describe('mutation activeUserMutations.userWorkspaceMutations', () => {
describe('leave', () => {
it('allows the active user to leave a workspace', async () => {
const name = cryptoRandomString({ length: 6 })
const workspaceCreateResult = await apollo.execute(CreateWorkspaceDocument, {
input: { name, slug: cryptoRandomString({ length: 10 }) }
})
expect(workspaceCreateResult).to.not.haveGraphQLErrors()
const id = workspaceCreateResult.data?.workspaceMutations.create.id
if (!id) throw new Error('This should have succeeded')
const updateWorkspaceRole = await apollo.execute(
UpdateWorkspaceRoleDocument,
{
input: {
userId: testMemberUser.id,
workspaceId: id,
role: Roles.Workspace.Admin
}
}
)
expect(updateWorkspaceRole).to.not.haveGraphQLErrors()
let userWorkspaces = await apollo.execute(GetActiveUserWorkspacesDocument, {})
expect(
userWorkspaces.data?.activeUser?.workspaces.items
.map((i) => i.name)
.includes(name)
).to.be.true
const leaveResult = await apollo.execute(ActiveUserLeaveWorkspaceDocument, {
id
})
expect(leaveResult.errors).to.be.undefined
userWorkspaces = await apollo.execute(GetActiveUserWorkspacesDocument, {})
expect(
userWorkspaces.data?.activeUser?.workspaces.items
.map((i) => i.name)
.includes(name)
).to.be.false
})
it('stops the last workspace admin from leaving the workspace', async () => {
const name = cryptoRandomString({ length: 6 })
const workspaceCreateResult = await apollo.execute(CreateWorkspaceDocument, {
input: { name, slug: cryptoRandomString({ length: 10 }) }
})
const id = workspaceCreateResult.data?.workspaceMutations.create.id
if (!id) throw new Error('This should have succeeded')
const leaveResult = await apollo.execute(ActiveUserLeaveWorkspaceDocument, {
id
})
expect(leaveResult.errors?.length).to.be.greaterThanOrEqual(1)
const userWorkspaces = await apollo.execute(
GetActiveUserWorkspacesDocument,
{}
)
expect(
userWorkspaces.data?.activeUser?.workspaces.items
.map((i) => i.name)
.includes(name)
).to.be.true
})
})
})
describe('mutation workspaceMutations.deleteDomain', () => {
it('should disable discoverability and domain protection when deleting last domain', async () => {
const workspaceName = cryptoRandomString({ length: 6 })
const createRes = await apollo.execute(CreateWorkspaceDocument, {
input: { name: workspaceName, slug: cryptoRandomString({ length: 10 }) }
})
expect(createRes).to.not.haveGraphQLErrors()
const workspaceId = createRes.data!.workspaceMutations.create.id
const addDomainRes = await apollo.execute(AddWorkspaceDomainDocument, {
input: {
workspaceId,
domain: 'example.org'
}
})
expect(addDomainRes).to.not.haveGraphQLErrors()
// Enable domain protection and discoverability
const getRes = await apollo.execute(UpdateWorkspaceDocument, {
input: {
id: workspaceId,
domainBasedMembershipProtectionEnabled: true,
discoverabilityEnabled: true
}
})
expect(getRes).to.not.haveGraphQLErrors()
const deleteDomainRes = await apollo.execute(DeleteWorkspaceDomainDocument, {
input: {
workspaceId,
id: addDomainRes.data!.workspaceMutations.addDomain.domains?.[0]?.id ?? ''
}
})
expect(deleteDomainRes).to.not.haveGraphQLErrors()
expect(
deleteDomainRes.data?.workspaceMutations.deleteDomain.discoverabilityEnabled
).to.false
expect(
deleteDomainRes.data?.workspaceMutations.deleteDomain
.domainBasedMembershipProtectionEnabled
).to.false
})
})
})
})