08e941f8af
* 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>
427 lines
13 KiB
TypeScript
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
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|