Files
speckle-server/packages/server/modules/workspaces/tests/integration/roles.graph.spec.ts
T
Kristaps Fabians Geikins 4b06f42db7 chore(server): run TS files directly (no compilation) (#5134)
* sort of works

* type fixes

* added option to run old way too
2025-07-23 11:20:40 +02:00

1233 lines
42 KiB
TypeScript

import { Streams } from '@/modules/core/dbSchema'
import { AllScopes } from '@/modules/core/helpers/mainConstants'
import { ProjectRecordVisibility } from '@/modules/core/helpers/types'
import type { BasicTestWorkspace } from '@/modules/workspaces/tests/helpers/creation'
import {
assignToWorkspace,
createTestWorkspace,
unassignFromWorkspace
} from '@/modules/workspaces/tests/helpers/creation'
import type { ProjectImplicitRoleCheck } from '@/modules/workspaces/tests/helpers/rolesGraphql'
import { projectImplicitRoleCheck } from '@/modules/workspaces/tests/helpers/rolesGraphql'
import { WorkspaceSeatType } from '@/modules/workspacesCore/domain/types'
import {
WorkspaceAcl,
Workspaces,
WorkspaceSeats
} from '@/modules/workspacesCore/helpers/db'
import type { BasicTestUser } from '@/test/authHelper'
import { createAuthTokenForUser, createTestUsers } from '@/test/authHelper'
import {
ActiveUserLeaveWorkspaceDocument,
GetUserProjectsWithAccessChecksDocument,
GetUserWorkspaceAccessDocument,
GetUserWorkspaceProjectsWithAccessChecksDocument,
GetWorkspaceDocument,
UpdateWorkspaceRoleDocument,
UpdateWorkspaceSeatTypeDocument
} from '@/modules/core/graph/generated/graphql'
import type { TestApolloServer } from '@/test/graphqlHelper'
import { createTestContext, testApolloServer } from '@/test/graphqlHelper'
import { beforeEachContext, truncateTables } from '@/test/hooks'
import type { BasicTestStream } from '@/test/speckle-helpers/streamHelper'
import { addToStream, createTestStream } from '@/test/speckle-helpers/streamHelper'
import { Roles } from '@speckle/shared'
import { expect } from 'chai'
import cryptoRandomString from 'crypto-random-string'
describe('Workspaces Roles/Seats GQL', () => {
let apollo: TestApolloServer
const serverAdminUser: BasicTestUser = {
id: '',
name: 'John Speckle',
email: 'john-speckle-workspace-admin@example.org',
role: Roles.Server.Admin
}
const serverMemberUser: BasicTestUser = {
id: '',
name: 'James Speckle',
email: 'james-speckle-workspace-member@example.org',
role: Roles.Server.User
}
before(async () => {
await beforeEachContext()
await createTestUsers([serverAdminUser, serverMemberUser])
const token = await createAuthTokenForUser(serverAdminUser.id, AllScopes)
apollo = await testApolloServer({
context: await createTestContext({
auth: true,
userId: serverAdminUser.id,
token,
role: serverAdminUser.role,
scopes: AllScopes
})
})
})
const getWorkspaceProjects = async (params: {
user: BasicTestUser
workspace: BasicTestWorkspace
}) => {
const res = await apollo.execute(
GetUserWorkspaceProjectsWithAccessChecksDocument,
{
id: params.workspace.id
},
{ authUserId: params.user.id, assertNoErrors: true }
)
const projects = res.data?.workspace.projects.items || []
expect(res.data?.workspace, 'Could not retrieve workspace for user').to.be.ok
return {
projects,
workspace: res.data!.workspace,
checkProject: (project: BasicTestStream) => {
return projectImplicitRoleCheck(projects.find((p) => p.id === project.id))
},
checkAllProjects: (check: (project: ProjectImplicitRoleCheck) => boolean) => {
return projects.map(projectImplicitRoleCheck).every(check)
}
}
}
const getUserProjects = async (params: { user: BasicTestUser }) => {
const res = await apollo.execute(
GetUserProjectsWithAccessChecksDocument,
{
filter: {
includeImplicitAccess: true
}
},
{ authUserId: params.user.id, assertNoErrors: true }
)
const projects = res.data?.activeUser?.projects.items || []
return {
projects,
checkProject: (project: BasicTestStream) => {
return projectImplicitRoleCheck(projects.find((p) => p.id === project.id))
},
checkAllProjects: (check: (project: ProjectImplicitRoleCheck) => boolean) => {
return projects.map(projectImplicitRoleCheck).every(check)
}
}
}
const getUserWorkspace = async (params: {
user: BasicTestUser
workspace: BasicTestWorkspace
}) => {
const res = await apollo.execute(
GetUserWorkspaceAccessDocument,
{
id: params.workspace.id
},
{ authUserId: params.user.id }
)
const workspace = res.data?.workspace
return {
workspace
}
}
describe('single role changes in a workspace without projects', () => {
const workspace: BasicTestWorkspace = {
id: '',
ownerId: '',
slug: cryptoRandomString({ length: 10 }),
name: 'My Test Workspace'
}
before(async () => {
await createTestWorkspace(workspace, serverAdminUser, {
addPlan: {
name: 'team',
status: 'valid'
}
})
})
describe('update workspace role', () => {
after(async () => {
await apollo.execute(
UpdateWorkspaceRoleDocument,
{
input: {
userId: serverMemberUser.id,
workspaceId: workspace.id,
role: null
}
},
{ assertNoErrors: true }
)
})
it('should create a role if none exists', async () => {
const res = await apollo.execute(UpdateWorkspaceRoleDocument, {
input: {
userId: serverMemberUser.id,
workspaceId: workspace.id,
role: Roles.Workspace.Admin
}
})
const { data } = await apollo.execute(GetWorkspaceDocument, {
workspaceId: workspace.id
})
const userRole = data?.workspace.team.items.find(
(user) => user.id === serverMemberUser.id
)
expect(res).to.not.haveGraphQLErrors()
expect(userRole).to.exist
expect(userRole?.role).to.equal(Roles.Workspace.Admin)
})
it('should update a role that exists', async () => {
const res = await apollo.execute(UpdateWorkspaceRoleDocument, {
input: {
userId: serverMemberUser.id,
workspaceId: workspace.id,
role: Roles.Workspace.Member
}
})
const roles = res.data?.workspaceMutations.updateRole.team.items
expect(res).to.not.haveGraphQLErrors()
expect(roles?.some((role) => role.id === serverMemberUser.id)).to.be.true
})
it('should throw if setting an invalid role', async () => {
const res = await apollo.execute(UpdateWorkspaceRoleDocument, {
input: {
userId: serverMemberUser.id,
workspaceId: workspace.id,
role: 'not-a-role'
}
})
expect(res).to.haveGraphQLErrors('Invalid workspace role')
})
it('should throw if attempting to remove last admin', async () => {
const res = await apollo.execute(UpdateWorkspaceRoleDocument, {
input: {
userId: serverAdminUser.id,
workspaceId: workspace.id,
role: Roles.Workspace.Member
}
})
expect(res).to.haveGraphQLErrors('last admin')
})
})
describe('delete workspace role', () => {
before(async () => {
await apollo.execute(
UpdateWorkspaceRoleDocument,
{
input: {
userId: serverMemberUser.id,
workspaceId: workspace.id,
role: Roles.Workspace.Member
}
},
{ assertNoErrors: true }
)
})
it('should delete the specified role', async () => {
const res = await apollo.execute(UpdateWorkspaceRoleDocument, {
input: {
userId: serverMemberUser.id,
workspaceId: workspace.id,
role: null
}
})
const roles = res.data?.workspaceMutations.updateRole.team.items
expect(res).to.not.haveGraphQLErrors()
expect(roles?.some((role) => role.id === serverMemberUser.id)).to.be.false
})
it('should throw if attempting to remove last admin', async () => {
const res = await apollo.execute(UpdateWorkspaceRoleDocument, {
input: {
userId: serverAdminUser.id,
workspaceId: workspace.id,
role: null
}
})
expect(res).to.haveGraphQLErrors('last admin')
})
})
})
describe('in a workspace with projects', () => {
const workspace: BasicTestWorkspace = {
id: '',
ownerId: '',
slug: cryptoRandomString({ length: 10 }),
name: 'Test Workspace'
}
const workspaceAdminUser: BasicTestUser = {
id: '',
name: 'John "Owner" Specke',
email: 'john-owner-speckle@example.org'
}
const workspaceMemberUser: BasicTestUser = {
id: '',
name: 'John "Member" Speckel',
email: 'john-member-speckle@example.org'
}
const workspaceGuestUser: BasicTestUser = {
id: '',
name: 'John "Middle Child" Speckle',
email: 'john-guest-speckle@example.org'
}
const workspaceMemberViewerUser: BasicTestUser = {
id: '',
name: 'John "Member" Viewer Speckel',
email: 'john-member-speckle-viewer@example.org'
}
const workspaceGuestViewerUser: BasicTestUser = {
id: '',
name: 'John "Middle Child" Viewer Speckle',
email: 'john-guest-speckle-viewer@example.org'
}
const workspaceProjectA: BasicTestStream = {
id: '',
ownerId: '',
name: 'Project A',
visibility: ProjectRecordVisibility.Workspace
}
const workspaceProjectB: BasicTestStream = {
id: '',
ownerId: '',
name: 'Project B',
visibility: ProjectRecordVisibility.Workspace
}
const workspaceProjectC: BasicTestStream = {
id: '',
ownerId: '',
name: 'Project C',
visibility: ProjectRecordVisibility.Workspace
}
const workspaceProjectD: BasicTestStream = {
id: '',
ownerId: '',
name: 'Project D',
visibility: ProjectRecordVisibility.Workspace
}
const workspaceProjectE: BasicTestStream = {
id: '',
ownerId: '',
name: 'Project E (Fully private)',
visibility: ProjectRecordVisibility.Private
}
const workspaceProjects = [
workspaceProjectA,
workspaceProjectB,
workspaceProjectC,
workspaceProjectD,
workspaceProjectE
]
before(async () => {
await createTestUsers([
workspaceAdminUser,
workspaceMemberUser,
workspaceGuestUser,
workspaceMemberViewerUser,
workspaceGuestViewerUser
])
})
beforeEach(async () => {
await createTestWorkspace(workspace, serverAdminUser, {
addPlan: {
name: 'team',
status: 'valid'
}
})
await Promise.all([
assignToWorkspace(
workspace,
workspaceAdminUser,
Roles.Workspace.Admin,
WorkspaceSeatType.Editor
),
assignToWorkspace(
workspace,
workspaceMemberUser,
Roles.Workspace.Member,
WorkspaceSeatType.Editor
),
assignToWorkspace(
workspace,
workspaceGuestUser,
Roles.Workspace.Guest,
WorkspaceSeatType.Editor
),
assignToWorkspace(
workspace,
workspaceMemberViewerUser,
Roles.Workspace.Member,
WorkspaceSeatType.Viewer
),
assignToWorkspace(
workspace,
workspaceGuestViewerUser,
Roles.Workspace.Guest,
WorkspaceSeatType.Viewer
)
])
for (const project of workspaceProjects) {
project.workspaceId = workspace.id
await createTestStream(project, serverAdminUser)
}
/**
* Initial workspace roles:
*
* workspaceAdminUser Admin (Editor)
* workspaceMemberUser Member (Editor)
* workspaceGuestUser Guest (Editor)
* workspaceMemberViewerUser Member (Viewer)
* workspaceGuestViewerUser Guest (Viewer)
*
* Initial explicit workspace project roles:
*
* | | Project A | Project B | Project C | Project D | Project E (private) |
* |---------------------------|-------------|-------------|-----------|-----------|---------------------|
* | workspaceAdminUser | Owner | None | None | None | None
* | workspaceMemberUser | Owner | Contributor | Reviewer | None | None
* | workspaceGuestUser | Contributor | Reviewer | None | None | Reviewer
* | workspaceMemberViewerUser | Reviewer | None | None | None | Reviewer
* | workspaceGuestViewerUser | None | Reviewer | None | None | Reviewer
*/
await Promise.all([
// A
addToStream(workspaceProjectA, workspaceAdminUser, Roles.Stream.Owner),
addToStream(workspaceProjectA, workspaceMemberUser, Roles.Stream.Owner),
addToStream(workspaceProjectA, workspaceGuestUser, Roles.Stream.Contributor),
addToStream(
workspaceProjectA,
workspaceMemberViewerUser,
Roles.Stream.Reviewer
),
// B
addToStream(workspaceProjectB, workspaceMemberUser, Roles.Stream.Contributor),
addToStream(workspaceProjectB, workspaceGuestUser, Roles.Stream.Reviewer),
addToStream(workspaceProjectB, workspaceGuestViewerUser, Roles.Stream.Reviewer),
// C
addToStream(workspaceProjectC, workspaceMemberUser, Roles.Stream.Reviewer),
// E
addToStream(workspaceProjectE, workspaceGuestUser, Roles.Stream.Reviewer),
addToStream(
workspaceProjectE,
workspaceMemberViewerUser,
Roles.Stream.Reviewer
),
addToStream(workspaceProjectE, workspaceGuestViewerUser, Roles.Stream.Reviewer)
])
})
afterEach(async () => {
await truncateTables([
Workspaces.name,
Streams.name,
WorkspaceAcl.name,
WorkspaceSeats.name
])
})
const getProjects = async (params: { user: BasicTestUser }) =>
getWorkspaceProjects({ user: params.user, workspace })
describe('retrieving projects', () => {
it('workspaceAdminUser is implicit owner of all of them and explicit owner in one', async () => {
const { projects, checkProject, checkAllProjects } = await getProjects({
user: workspaceAdminUser
})
expect(projects.length).to.eq(5)
expect(checkAllProjects((p) => p.isOwner)).to.be.ok
expect(checkProject(workspaceProjectA).isExplicitOwner).to.be.ok
expect(checkProject(workspaceProjectB).hasExplicitRole).to.be.not.ok
expect(checkProject(workspaceProjectC).hasExplicitRole).to.be.not.ok
expect(checkProject(workspaceProjectD).hasExplicitRole).to.be.not.ok
expect(checkProject(workspaceProjectE).hasExplicitRole).to.be.not.ok
})
it('workspaceMemberUser is implicit reviewer in all of them, except E, and also has explicit roles in some', async () => {
const { projects, checkAllProjects, checkProject } = await getProjects({
user: workspaceMemberUser
})
expect(projects.length).to.eq(4)
expect(checkAllProjects((p) => p.isReviewer)).to.be.ok
expect(checkProject(workspaceProjectA).isExplicitOwner).to.be.ok
expect(checkProject(workspaceProjectB).isExplicitContributor).to.be.ok
expect(checkProject(workspaceProjectC).isExplicitReviewer).to.be.ok
expect(checkProject(workspaceProjectD).hasExplicitRole).to.be.not.ok
expect(checkProject(workspaceProjectE).hasAccess).to.be.not.ok
})
it('workspaceGuestUser only has explicit roles in 3 projects', async () => {
const { projects, checkProject } = await getProjects({
user: workspaceGuestUser
})
expect(projects.length).to.eq(3)
expect(checkProject(workspaceProjectA).isExplicitContributor).to.be.ok
expect(checkProject(workspaceProjectB).isExplicitReviewer).to.be.ok
expect(checkProject(workspaceProjectC).hasAccess).to.be.not.ok
expect(checkProject(workspaceProjectD).hasAccess).to.be.not.ok
expect(checkProject(workspaceProjectE).isExplicitReviewer).to.be.ok
})
it('workspaceMemberViewerUser is only explicit reviewer in 2 projects, and has implicit roles elsewhere', async () => {
const { projects, checkAllProjects, checkProject } = await getProjects({
user: workspaceMemberViewerUser
})
expect(projects.length).to.eq(5)
expect(checkAllProjects((p) => p.isReviewer)).to.be.ok
expect(checkProject(workspaceProjectA).isExplicitReviewer).to.be.ok
expect(checkProject(workspaceProjectB).hasExplicitRole).to.be.not.ok
expect(checkProject(workspaceProjectC).hasExplicitRole).to.be.not.ok
expect(checkProject(workspaceProjectD).hasExplicitRole).to.be.not.ok
expect(checkProject(workspaceProjectE).isExplicitReviewer).to.be.ok
})
it('workspaceGuestViewerUser is only explicit reviewer in 2 projects', async () => {
const { projects, checkProject } = await getProjects({
user: workspaceGuestViewerUser
})
expect(projects.length).to.eq(2)
expect(checkProject(workspaceProjectB).isExplicitReviewer).to.be.ok
expect(checkProject(workspaceProjectA).hasExplicitRole).to.be.not.ok
expect(checkProject(workspaceProjectC).hasExplicitRole).to.be.not.ok
expect(checkProject(workspaceProjectD).hasExplicitRole).to.be.not.ok
expect(checkProject(workspaceProjectE).isExplicitReviewer).to.be.ok
})
})
describe('doing single seat type changes', () => {
it('cant change workspace admin to viewer', async () => {
const res = await apollo.execute(UpdateWorkspaceSeatTypeDocument, {
input: {
userId: workspaceAdminUser.id,
workspaceId: workspace.id,
seatType: WorkspaceSeatType.Viewer
}
})
expect(res).to.haveGraphQLErrors('cannot have a seat of type')
})
it('changing member editor to viewer, should downgrade all explicit roles to reviewer', async () => {
await apollo.execute(
UpdateWorkspaceSeatTypeDocument,
{
input: {
userId: workspaceMemberUser.id,
workspaceId: workspace.id,
seatType: WorkspaceSeatType.Viewer
}
},
{ assertNoErrors: true }
)
const { projects, checkProject } = await getProjects({
user: workspaceMemberUser
})
expect(projects.length).to.eq(4)
expect(checkProject(workspaceProjectA).isExplicitReviewer).to.be.ok
expect(checkProject(workspaceProjectB).isExplicitReviewer).to.be.ok
expect(checkProject(workspaceProjectC).isExplicitReviewer).to.be.ok
expect(checkProject(workspaceProjectD).hasExplicitRole).to.be.not.ok
})
it('changing guest editor to viewer, should downgrade all explicit roles to reviewer', async () => {
await apollo.execute(
UpdateWorkspaceSeatTypeDocument,
{
input: {
userId: workspaceGuestUser.id,
workspaceId: workspace.id,
seatType: WorkspaceSeatType.Viewer
}
},
{ assertNoErrors: true }
)
const { projects, checkProject } = await getProjects({
user: workspaceGuestUser
})
expect(projects.length).to.eq(3)
expect(checkProject(workspaceProjectA).isExplicitReviewer).to.be.ok
expect(checkProject(workspaceProjectB).isExplicitReviewer).to.be.ok
expect(checkProject(workspaceProjectE).isExplicitReviewer).to.be.ok
})
})
describe('doing single role changes', () => {
describe('when changing workspace admin', () => {
describe('to workspace member', () => {
beforeEach(async () => {
await apollo.execute(
UpdateWorkspaceRoleDocument,
{
input: {
userId: workspaceAdminUser.id,
workspaceId: workspace.id,
role: Roles.Workspace.Member
}
},
{ assertNoErrors: true }
)
})
it('should still remain explicit owner and be implicit reviewer elsewhere, except private E', async () => {
const { projects, checkAllProjects, checkProject } = await getProjects({
user: workspaceAdminUser
})
expect(projects.length).to.eq(4)
expect(checkAllProjects((p) => p.isReviewer)).to.be.ok
expect(checkProject(workspaceProjectA).isExplicitOwner).to.be.ok
expect(checkProject(workspaceProjectB).hasExplicitRole).to.be.not.ok
expect(checkProject(workspaceProjectC).hasExplicitRole).to.be.not.ok
expect(checkProject(workspaceProjectD).hasExplicitRole).to.be.not.ok
expect(checkProject(workspaceProjectE).hasAccess).to.be.not.ok
})
})
describe('to workspace guest', () => {
beforeEach(async () => {
await apollo.execute(
UpdateWorkspaceRoleDocument,
{
input: {
userId: workspaceAdminUser.id,
workspaceId: workspace.id,
role: Roles.Workspace.Guest
}
},
{ assertNoErrors: true }
)
})
it('should only have 1 project access, and not owner, but contributor', async () => {
const { projects, checkProject } = await getProjects({
user: workspaceAdminUser
})
expect(projects.length).to.eq(1)
expect(checkProject(workspaceProjectA).isOwner).to.not.be.ok
expect(checkProject(workspaceProjectA).isExplicitContributor).to.be.ok
expect(checkProject(workspaceProjectB).hasExplicitRole).to.be.not.ok
expect(checkProject(workspaceProjectC).hasExplicitRole).to.be.not.ok
expect(checkProject(workspaceProjectD).hasExplicitRole).to.be.not.ok
})
})
})
describe('when changing workspace member', () => {
describe('to workspace admin', () => {
beforeEach(async () => {
await apollo.execute(
UpdateWorkspaceRoleDocument,
{
input: {
userId: workspaceMemberUser.id,
workspaceId: workspace.id,
role: Roles.Workspace.Admin
}
},
{ assertNoErrors: true }
)
})
it('should get implicit owner role everywhere and explicit upgraded to owner', async () => {
const { projects, checkProject, checkAllProjects } = await getProjects({
user: workspaceMemberUser
})
expect(projects.length).to.eq(5)
expect(checkAllProjects((p) => p.isOwner)).to.be.ok
expect(checkProject(workspaceProjectA).isExplicitOwner).to.be.ok
expect(checkProject(workspaceProjectB).isExplicitOwner).to.be.ok
expect(checkProject(workspaceProjectC).isExplicitOwner).to.be.ok
expect(checkProject(workspaceProjectD).hasExplicitRole).to.not.be.ok
expect(checkProject(workspaceProjectE).hasExplicitRole).to.not.be.ok
})
})
describe('to workspace guest', () => {
beforeEach(async () => {
await apollo.execute(
UpdateWorkspaceRoleDocument,
{
input: {
userId: workspaceMemberUser.id,
workspaceId: workspace.id,
role: Roles.Workspace.Guest
}
},
{ assertNoErrors: true }
)
})
it('no implicit access and all explicit downgraded to contributor or less', async () => {
const { projects, checkProject } = await getProjects({
user: workspaceMemberUser
})
expect(projects.length).to.eq(3)
expect(checkProject(workspaceProjectA).isExplicitContributor).to.be.ok
expect(checkProject(workspaceProjectB).isExplicitContributor).to.be.ok
expect(checkProject(workspaceProjectC).isExplicitReviewer).to.be.ok
expect(checkProject(workspaceProjectD).hasExplicitRole).to.be.not.ok
expect(checkProject(workspaceProjectE).hasExplicitRole).to.be.not.ok
})
})
})
describe('when changing workspace guest', () => {
describe('to workspace admin', () => {
beforeEach(async () => {
await apollo.execute(
UpdateWorkspaceRoleDocument,
{
input: {
userId: workspaceGuestUser.id,
workspaceId: workspace.id,
role: Roles.Workspace.Admin
}
},
{ assertNoErrors: true }
)
})
it('should upgrade explicit role to owner, and have implicit owner everywhere', async () => {
const { projects, checkProject, checkAllProjects } = await getProjects({
user: workspaceGuestUser
})
expect(projects.length).to.eq(5)
expect(checkAllProjects((p) => p.isOwner)).to.be.ok
expect(checkProject(workspaceProjectA).isExplicitOwner).to.be.ok
expect(checkProject(workspaceProjectB).isExplicitOwner).to.be.ok
expect(checkProject(workspaceProjectC).hasExplicitRole).to.not.be.ok
expect(checkProject(workspaceProjectD).hasExplicitRole).to.be.not.ok
expect(checkProject(workspaceProjectE).isExplicitOwner).to.be.ok
})
})
describe('to workspace member', () => {
beforeEach(async () => {
await apollo.execute(
UpdateWorkspaceRoleDocument,
{
input: {
userId: workspaceGuestUser.id,
workspaceId: workspace.id,
role: Roles.Workspace.Member
}
},
{ assertNoErrors: true }
)
})
it('should retain same explicit access and get full implicit acccess', async () => {
const { projects, checkProject, checkAllProjects } = await getProjects({
user: workspaceGuestUser
})
expect(projects.length).to.eq(5)
expect(checkAllProjects((p) => p.isReviewer)).to.be.ok
expect(checkProject(workspaceProjectA).isExplicitContributor).to.be.ok
expect(checkProject(workspaceProjectB).isExplicitReviewer).to.be.ok
expect(checkProject(workspaceProjectC).hasExplicitRole).to.be.not.ok
expect(checkProject(workspaceProjectD).hasExplicitRole).to.be.not.ok
expect(checkProject(workspaceProjectE).isExplicitReviewer).to.be.ok
})
})
})
describe('when changing workspace member viewer', () => {
describe('to workspace admin', () => {
beforeEach(async () => {
await apollo.execute(
UpdateWorkspaceRoleDocument,
{
input: {
userId: workspaceMemberViewerUser.id,
workspaceId: workspace.id,
role: Roles.Workspace.Admin
}
},
{ assertNoErrors: true }
)
})
it('should get editor seat, implicit owner role everywhere and explicit upgraded to owner', async () => {
const { workspace, projects, checkProject, checkAllProjects } =
await getProjects({
user: workspaceMemberViewerUser
})
expect(workspace.seatType).to.eq(WorkspaceSeatType.Editor)
expect(projects.length).to.eq(5)
expect(checkAllProjects((p) => p.isOwner)).to.be.ok
expect(checkProject(workspaceProjectA).isExplicitOwner).to.be.ok
expect(checkProject(workspaceProjectB).hasExplicitRole).to.not.be.ok
expect(checkProject(workspaceProjectC).hasExplicitRole).to.not.be.ok
expect(checkProject(workspaceProjectD).hasExplicitRole).to.not.be.ok
expect(checkProject(workspaceProjectE).isExplicitOwner).to.be.ok
})
})
describe('to workspace guest', () => {
beforeEach(async () => {
await apollo.execute(
UpdateWorkspaceRoleDocument,
{
input: {
userId: workspaceMemberViewerUser.id,
workspaceId: workspace.id,
role: Roles.Workspace.Guest
}
},
{ assertNoErrors: true }
)
})
it('retain viewer seat, no implicit access and all explicit at reviewer or less', async () => {
const { projects, checkProject, workspace } = await getProjects({
user: workspaceMemberViewerUser
})
expect(workspace.seatType).to.eq(WorkspaceSeatType.Viewer)
expect(projects.length).to.eq(2)
expect(checkProject(workspaceProjectA).isExplicitReviewer).to.be.ok
expect(checkProject(workspaceProjectE).isExplicitReviewer).to.be.ok
})
})
})
describe('when changing workspace guest viewer', () => {
describe('to workspace admin', () => {
beforeEach(async () => {
await apollo.execute(
UpdateWorkspaceRoleDocument,
{
input: {
userId: workspaceGuestViewerUser.id,
workspaceId: workspace.id,
role: Roles.Workspace.Admin
}
},
{ assertNoErrors: true }
)
})
it('should upgrade seatType to editor, explicit role to owner, and have implicit owner everywhere', async () => {
const { workspace, projects, checkProject, checkAllProjects } =
await getProjects({
user: workspaceGuestViewerUser
})
expect(workspace.seatType).to.eq(WorkspaceSeatType.Editor)
expect(projects.length).to.eq(5)
expect(checkAllProjects((p) => p.isOwner)).to.be.ok
expect(checkProject(workspaceProjectA).hasExplicitRole).to.not.be.ok
expect(checkProject(workspaceProjectB).isExplicitOwner).to.be.ok
expect(checkProject(workspaceProjectC).hasExplicitRole).to.not.be.ok
expect(checkProject(workspaceProjectD).hasExplicitRole).to.be.not.ok
expect(checkProject(workspaceProjectE).isExplicitOwner).to.be.ok
})
})
describe('to workspace member', () => {
beforeEach(async () => {
await apollo.execute(
UpdateWorkspaceRoleDocument,
{
input: {
userId: workspaceGuestViewerUser.id,
workspaceId: workspace.id,
role: Roles.Workspace.Member
}
},
{ assertNoErrors: true }
)
})
it('should retain viewer seat, same explicit access and get full workspace visibility implicit acccess', async () => {
const { workspace, projects, checkProject, checkAllProjects } =
await getProjects({
user: workspaceGuestViewerUser
})
expect(workspace.seatType).to.eq(WorkspaceSeatType.Viewer)
expect(projects.length).to.eq(5)
expect(checkAllProjects((p) => p.isReviewer)).to.be.ok
expect(checkProject(workspaceProjectA).hasExplicitRole).to.be.not.ok
expect(checkProject(workspaceProjectB).isExplicitReviewer).to.be.ok
expect(checkProject(workspaceProjectC).hasExplicitRole).to.be.not.ok
expect(checkProject(workspaceProjectD).hasExplicitRole).to.be.not.ok
expect(checkProject(workspaceProjectE).isExplicitReviewer).to.be.ok
})
})
})
})
})
describe('doing composite role/seat changes', () => {
const testWorkspace: BasicTestWorkspace = {
id: '',
ownerId: '',
slug: '',
name: 'Test Composite Role Change Workspace'
}
const workspaceAdminUser: BasicTestUser = {
id: '',
name: 'Composite John "Owner" Specke',
email: 'composite-john-owner-speckle@example.org'
}
const workspaceMemberUser: BasicTestUser = {
id: '',
name: 'Composite John "Member" Speckel',
email: 'composite-john-member-speckle@example.org'
}
before(async () => {
await createTestUsers([workspaceAdminUser, workspaceMemberUser])
})
beforeEach(async () => {
await createTestWorkspace(testWorkspace, serverAdminUser, {
addPlan: {
name: 'team',
status: 'valid'
}
})
await assignToWorkspace(
testWorkspace,
workspaceAdminUser,
Roles.Workspace.Admin,
WorkspaceSeatType.Editor
)
await assignToWorkspace(
testWorkspace,
workspaceMemberUser,
Roles.Workspace.Member,
WorkspaceSeatType.Editor
)
})
afterEach(async () => {
await truncateTables([
Workspaces.name,
Streams.name,
WorkspaceAcl.name,
WorkspaceSeats.name
])
})
it('downgrading admin->guest if last owner, sets new owner from workspace admins', async () => {
// User Workspace Role Project Role
// serverAdminUser Admin None
// workspaceAdminUser Admin Owner
//
// Action: `workspaceAdminUser` downgraded to workspace guest
const project: BasicTestStream = {
id: '',
ownerId: '',
name: 'Test Composite Project',
isPublic: false,
workspaceId: testWorkspace.id
}
await createTestStream(project, workspaceAdminUser)
const apollo = await testApolloServer({
authUserId: serverAdminUser.id
})
const remove = await apollo.execute(UpdateWorkspaceRoleDocument, {
input: {
userId: workspaceAdminUser.id,
role: Roles.Workspace.Guest,
workspaceId: testWorkspace.id
}
})
expect(remove).to.not.haveGraphQLErrors()
const { workspace, checkProject } = await getWorkspaceProjects({
user: workspaceAdminUser,
workspace: testWorkspace
})
expect(workspace?.role).to.eq(Roles.Workspace.Guest)
expect(checkProject(project).isExplicitContributor).to.be.ok
const { checkProject: checkProjectForAdmin } = await getUserProjects({
user: serverAdminUser
})
expect(checkProjectForAdmin(project).isExplicitOwner).to.be.ok
})
it('downgrading member to viewer if last owner, sets new owner from workspace admins', async () => {
// User Workspace Role Project Role
// workspaceAdminUser Admin None
// workspaceMemberUser Member Owner
//
// Action: `workspaceAdminUser` downgraded to workspace guest
// ensure serverAdmin is no longer admin, so there's only 1 - workspaceAdmin
await unassignFromWorkspace(testWorkspace, serverAdminUser)
const project: BasicTestStream = {
id: '',
ownerId: '',
name: 'Test Composite Project',
isPublic: false,
workspaceId: testWorkspace.id
}
await createTestStream(project, workspaceMemberUser)
const apollo = await testApolloServer({
authUserId: workspaceAdminUser.id
})
const downgrade = await apollo.execute(UpdateWorkspaceSeatTypeDocument, {
input: {
userId: workspaceMemberUser.id,
workspaceId: testWorkspace.id,
seatType: WorkspaceSeatType.Viewer
}
})
expect(downgrade).to.not.haveGraphQLErrors()
const { workspace, checkProject } = await getWorkspaceProjects({
user: workspaceMemberUser,
workspace: testWorkspace
})
expect(workspace?.role).to.eq(Roles.Workspace.Member)
expect(workspace?.seatType).to.eq(WorkspaceSeatType.Viewer)
expect(checkProject(project).isExplicitReviewer).to.be.ok
const { checkProject: checkProjectForAdmin } = await getUserProjects({
user: workspaceAdminUser
})
expect(checkProjectForAdmin(project).isExplicitOwner).to.be.ok
})
it('leaving workspace as last owner of a workspace, sets new owner from workspace admins', async () => {
// User Workspace Role Project Role
// workspaceAdminUser Admin None
// workspaceMemberUser Member Owner
//
// Action: `workspaceMemberUser` leaves workspace
// ensure serverAdmin is no longer admin, so there's only 1 - workspaceAdmin
await unassignFromWorkspace(testWorkspace, serverAdminUser)
const project: BasicTestStream = {
id: '',
ownerId: '',
name: 'Test Leave Project',
isPublic: false,
workspaceId: testWorkspace.id
}
await createTestStream(project, workspaceMemberUser)
const apollo = await testApolloServer({
authUserId: workspaceMemberUser.id
})
const leave = await apollo.execute(ActiveUserLeaveWorkspaceDocument, {
id: testWorkspace.id
})
expect(leave).to.not.haveGraphQLErrors()
const { workspace } = await getUserWorkspace({
user: workspaceMemberUser,
workspace: testWorkspace
})
expect(workspace?.role).to.not.be.ok
const { checkProject } = await getUserProjects({
user: workspaceMemberUser
})
expect(checkProject(project).hasExplicitRole).to.be.not.ok
const { checkProject: checkProjectForAdmin } = await getUserProjects({
user: workspaceAdminUser
})
expect(checkProjectForAdmin(project).isExplicitOwner).to.be.ok
})
it('leaving workspace w/o owner roles works fine and removes all roles', async () => {
// User Workspace Role Project Role
// workspaceAdminUser Admin Owner
// workspaceMemberUser Member Reviewer
//
// Action: `workspaceMemberUser` leaves workspace
const project: BasicTestStream = {
id: '',
ownerId: '',
name: 'Test Leave Project',
isPublic: false,
workspaceId: testWorkspace.id
}
await createTestStream(project, workspaceAdminUser)
await addToStream(project, workspaceMemberUser, Roles.Stream.Reviewer)
const apollo = await testApolloServer({
authUserId: workspaceMemberUser.id
})
const leave = await apollo.execute(ActiveUserLeaveWorkspaceDocument, {
id: testWorkspace.id
})
expect(leave).to.not.haveGraphQLErrors()
expect(leave.data?.workspaceMutations.leave).to.be.ok
const { checkProject } = await getUserProjects({
user: workspaceMemberUser
})
expect(checkProject(project).hasExplicitRole).to.be.not.ok
const { workspace } = await getUserWorkspace({
user: workspaceMemberUser,
workspace: testWorkspace
})
expect(workspace?.role).to.be.not.ok
})
it('removing a workspace member that is the last owner of a workspace project sets new owner from workspace admins', async () => {
// User Workspace Role Project Role
// workspaceAdminUser Admin None
// workspaceMemberUser Member Owner
//
// Action: `workspaceAdminUser` removes `workspaceMemberUser` from the workspace
// ensure serverAdmin is no longer admin, so there's only 1 - workspaceAdmin
await unassignFromWorkspace(testWorkspace, serverAdminUser)
const project: BasicTestStream = {
id: '',
ownerId: '',
name: 'Test Remove Project',
isPublic: false,
workspaceId: testWorkspace.id
}
await createTestStream(project, workspaceMemberUser)
const apollo = await testApolloServer({
authUserId: workspaceAdminUser.id
})
const remove = await apollo.execute(UpdateWorkspaceRoleDocument, {
input: {
userId: workspaceMemberUser.id,
role: null,
workspaceId: testWorkspace.id
}
})
expect(remove).to.not.haveGraphQLErrors()
expect(remove.data?.workspaceMutations.updateRole).to.be.ok
const { checkProject } = await getUserProjects({
user: workspaceMemberUser
})
expect(checkProject(project).hasExplicitRole).to.be.not.ok
const { workspace } = await getUserWorkspace({
user: workspaceMemberUser,
workspace: testWorkspace
})
expect(workspace?.role).to.be.not.ok
const { checkProject: checkProjectForAdmin } = await getUserProjects({
user: workspaceAdminUser
})
expect(checkProjectForAdmin(project).isExplicitOwner).to.be.ok
})
it('removing a workspace member that is not the last owner of a workspace project works fine and removes all roles', async () => {
// User Workspace Role Project Role
// workspaceAdminUser Admin Owner
// workspaceMemberUser Member Reviewer
//
// Action: `workspaceAdminUser` removes `workspaceMemberUser` from the workspace
const project: BasicTestStream = {
id: '',
ownerId: '',
name: 'Test Remove Project',
isPublic: false,
workspaceId: testWorkspace.id
}
await createTestStream(project, workspaceAdminUser)
await addToStream(project, workspaceMemberUser, Roles.Stream.Reviewer)
const apollo = await testApolloServer({
authUserId: workspaceAdminUser.id
})
const remove = await apollo.execute(UpdateWorkspaceRoleDocument, {
input: {
userId: workspaceMemberUser.id,
role: null,
workspaceId: testWorkspace.id
}
})
expect(remove).to.not.haveGraphQLErrors()
expect(remove.data?.workspaceMutations.updateRole).to.be.ok
const { checkProject } = await getUserProjects({
user: workspaceMemberUser
})
expect(checkProject(project).hasExplicitRole).to.be.not.ok
const { workspace } = await getUserWorkspace({
user: workspaceMemberUser,
workspace: testWorkspace
})
expect(workspace?.role).to.be.not.ok
})
})
})