Files
speckle-server/packages/server/modules/workspaces/tests/integration/workspaces.graph.spec.ts
T
Gergő Jedlicska 08e941f8af Poor man's SSO (#2641)
* Implemented workspace general page

* Added notifications to user input

* Allowed non-admins to view but not edit

* Added redirect to homeroute

* Fixed validation

* Squashed commit of the following:

commit 7bf14ab8af0f76b4c9d0aa87fc08085af7c34959
Author: Chuck Driesler <chuck@speckle.systems>
Date:   Tue Aug 6 19:40:50 2024 +0200

    mob next [ci-skip] [ci skip] [skip ci]

    lastFile:packages/server/modules/workspacesCore/migrations/20240806160740_workspace_domains.ts

commit 8aa3fb0cb052c10eeeb83bf9874ae0d1c065e480
Author: Alessandro Magionami <alessandro.magionami@gmail.com>
Date:   Tue Aug 6 18:54:15 2024 +0200

    mob next [ci-skip] [ci skip] [skip ci]

    lastFile:packages/server/modules/core/domain/userEmails/operations.ts

commit 66dfd0cf6c15a789c8f96a65a3168323e83a7b9e
Author: Chuck Driesler <chuck@speckle.systems>
Date:   Tue Aug 6 18:30:22 2024 +0200

    mob next [ci-skip] [ci skip] [skip ci]

    lastFile:packages/server/modules/workspacesCore/domain/types.ts

Co-authored-by: Alessandro Magionami <alessandro.magionami@gmail.com>

* Move General to workspaces folder

* feat(workspaces): inputs on security section

* feat(workspaces): add domain to workspace mutation

* chore(workspaces): add blocked domains list

* fix(workspaces): modals with buttons

* feat(workspaceDomains): delete domain

* fix(workspaces): use  mutation

* fix(workspaces): present user verified domains as options

* Moved sidebar menu to a composable

* Added coming soon tag back

* feat(workspaces): create domains resolver for workspace

* chore(workspaces): fix tests

* chore(workspaces): fix types

* chore(workspaces): fix linter

* fix(workspaces): do some delete I think

* chore(workspaces): add domainBasedMembershipProtectionEnabled field to workspace

* chore(workspaces): improve validation for email domain

* fix(workspace): query and do the thing

* chore(workspaces): add graphql schema for domainBasedMembershipProtection

* chore(workspaces): lint and test failures

* fix(workspaces): test issues w new field

* feat(workspaces): add discoverability flag

* chore(workspaces): they made me do it

* feat(workspaces): enable toggling domain protection

* feat(workspaces): add discoverability toggle to workspace settings

* feat(workspace): auto enable discoverability on first domain registration

* feat(workspace): discoverability toggle fixes

* fix(eventBus): fix tests

* feat(workspaces): user discoverable workspaces (#2620)

* feat(workspaces): it works just trust me

* fix(workspaces): don't worry about it

* fix(workspaces); happy path success

* fix(workspaces): almost there

* fix(workspaces): successful tests!

* fix(workspaces): we have DISCOVERED (#2621)

* Fixed linting issue

* Updated query

* Updated validation rules

* Updated validation rules

* Fix unsaved file with type export

* Addressed PR comments

* Updated cache

* Updated item classes, add fragment back

* Gergo/web 1574 join workspaces via discovery (#2623)

* chore(useremails): add find verified emails by user function

* chore(workspace): table helper for workspace domains

* chore(workspace): get workspace with domains function

* chore(workspace): test get workspace with domains function

* feat(workspace): restrict workspace membership when updating workspace role

* chore(workspaces): fix types

* feat(workspaces): WIP join

* feat(workspaces): join button makes u join

* chore(useremails): fix type for find verified emails function

* feat(workspaces): join

* feat(workspace): prevent inviting user without email matching domain

* chore(workspaces): fix linter

* fix(workspaces): invoke join (gergo wrote this)

* fuck

* fix(workspaces): properly get discoverable workspaces

* fix(workspaces): test

---------

Co-authored-by: Gergő Jedlicska <gergo@jedlicska.com>
Co-authored-by: Chuck Driesler <chuck@speckle.systems>

* fix(workspaces): some query stuff

* fix(workspaces): mutate cache instead of refetch

* fix(workspaces): more adjustments to gql query and fragment structure

* fix(workspaces): queries, style, structure

* fix(workspaces): match discoverability with current styles

* chore(workspaces): lint lint lint

* fix(workspaces): got it twisted

* chore(workspaces): fix test

* fix(workspaces): route to joined workspace on join

---------

Co-authored-by: Mike Tasset <mike.tasset@gmail.com>
Co-authored-by: Chuck Driesler <chuck@speckle.systems>
Co-authored-by: Alessandro Magionami <alessandro.magionami@gmail.com>
2024-08-26 13:33:16 +02:00

427 lines
13 KiB
TypeScript

import { expect } from 'chai'
import cryptoRandomString from 'crypto-random-string'
import {
createTestContext,
testApolloServer,
TestApolloServer
} from '@/test/graphqlHelper'
import {
BasicTestUser,
createAuthTokenForUser,
createTestUsers
} from '@/test/authHelper'
import { Roles } from '@speckle/shared'
import {
CreateProjectInviteDocument,
CreateWorkspaceDocument,
DeleteWorkspaceDocument,
GetActiveUserWorkspacesDocument,
GetWorkspaceDocument,
GetWorkspaceTeamDocument,
UpdateWorkspaceDocument,
UpdateWorkspaceRoleDocument,
ActiveUserLeaveWorkspaceDocument
} 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 knex from '@/db/knex'
describe('Workspaces GQL CRUD', () => {
let apollo: TestApolloServer
const testAdminUser: BasicTestUser = {
id: '',
name: 'John Speckle',
email: 'john-speckle@example.org',
role: Roles.Server.Admin
}
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: 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'
}
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', () => {
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', () => {
it('should return workspace members', async () => {
const res = await apollo.execute(GetWorkspaceTeamDocument, {
workspaceId: workspace.id
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.workspace.team.length).to.equal(2)
})
it('should respect search filters', async () => {
const res = await apollo.execute(GetWorkspaceTeamDocument, {
workspaceId: workspace.id,
filter: {
search: 'jimmy'
}
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.workspace.team.length).to.equal(1)
})
it('should respect role filters', async () => {
const res = await apollo.execute(GetWorkspaceTeamDocument, {
workspaceId: workspace.id,
filter: {
role: 'workspace:member'
}
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.workspace.team.length).to.equal(1)
})
})
describe('query activeUser.workspaces', () => {
it('should return all workspaces for a user', async () => {
const res = await apollo.execute(GetActiveUserWorkspacesDocument, {})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.activeUser?.workspaces?.items?.length).to.equal(1)
})
})
})
describe('management operations', () => {
describe('mutation workspaceMutations.create', () => {
it('should create a workspace', async () => {
const workspaceName = cryptoRandomString({ length: 6 })
const createRes = await apollo.execute(CreateWorkspaceDocument, {
input: { name: workspaceName }
})
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)
})
})
describe('mutation workspaceMutations.delete', () => {
const workspace: BasicTestWorkspace = {
id: '',
ownerId: '',
name: 'My Test Workspace'
}
const workspaceProject: BasicTestStream = {
id: '',
ownerId: '',
name: 'My Test Project',
isPublic: false
}
before(async () => {
await createTestWorkspace(workspace, testAdminUser)
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: ''
}
createTestCommit(testVersion, {
owner: testAdminUser,
stream: workspaceProject
})
})
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 memberApollo: TestApolloServer = (apollo = await testApolloServer({
context: createTestContext({
auth: true,
userId: testAdminUser.id,
token: '',
role: testAdminUser.role,
scopes: AllScopes
})
}))
const res = await memberApollo.execute(DeleteWorkspaceDocument, {
workspaceId: workspace.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: '',
ownerId: '',
name: cryptoRandomString({ length: 6 }),
description: cryptoRandomString({ length: 12 })
}
beforeEach(async () => {
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 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 }
})
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 }
})
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
})
})
})
})
})