Files
Daniel Gak Anagrov 75aa5d9b2d feat(ci): reinstate multiregion tests (#5365)
* feat(multiregion): replace user replication

* chore(multiregion): optimise replication

* maybe it's this

* postgres is fun

* once more

* chore(multiregion): only replicate test user creation during multiregion tests

* feat: improved replicate_query logic

* fix: minor

* fix: starting issue

* feat: included user create and delete specs to multiregion

* feat: removed console logs

* fix: user defaults

* fix: multiregion test helper

* fix: update scenarios for users

* refactor(multiregion): swap replicateQuery concept to asMultiregionOperation (#5301)

feat(multiregion): introduced asMultregionOperator, refactor test to user builder classes

* chore: renamings

* fix: remove comments

* feat: remove user replication

* refactor: simplified spec usages

* chore: comments

* chore: branches and favs

* chore: more tests

* chore: more tests

* fix linting

* fix tests

* feat: dropping replication

* refactor: moved project delete to service

* fix: comment

* feat: updateStreamFactory and updateProjectFacotry

* deleteProjectFactory + replicateFactory

* deleteWorkspaceFactory

* fix: selector

* fix: tests

* fix tests, finished createStreamFactory

* feat: simplify changes

* fix: remove comment

* fix: minor strucutres

* fix: moveProjectToRegion

* fix: moved branch creation outside of multiregion scope

* fix: branch creation

* fix: tests

* fix: ci tests

* fix: removed log form test

* fix: on specs, no random regionKeys

* feat: simplify ci for postgres

* try: fix health check

* feat: fixed tests in ci

* try: entrypoint

* try: entrypoint

* try: entrypoint

* try: POSTGRES_INITDB_ARGS

* feat: apply POSTGRES_INITDB_ARGS to all server tests

* fix: broken test

* fix: reinstate max health attempts

* fix: after merge

* fix: after merge

---------

Co-authored-by: Charles Driesler <chuck@speckle.systems>
2025-09-04 14:49:02 +02:00

1272 lines
43 KiB
TypeScript

import { db } from '@/db/knex'
import { StreamAcl, Streams } from '@/modules/core/dbSchema'
import type { StreamRecord } from '@/modules/core/helpers/types'
import { ProjectRecordVisibility } from '@/modules/core/helpers/types'
import { grantStreamPermissionsFactory } from '@/modules/core/repositories/streams'
import { WorkspaceSeatType } from '@/modules/gatekeeper/domain/billing'
import { getWorkspaceUserSeatsFactory } from '@/modules/gatekeeper/repositories/workspaceSeat'
import { getRegionDb } from '@/modules/multiregion/utils/dbSelector'
import { WorkspaceInvalidRoleError } from '@/modules/workspaces/errors/workspace'
import type { BasicTestWorkspace } from '@/modules/workspaces/tests/helpers/creation'
import {
assignToWorkspace,
assignToWorkspaces,
createTestWorkspace
} from '@/modules/workspaces/tests/helpers/creation'
import { describeEach, itEach } from '@/test/assertionHelper'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUser, createTestUsers, login } from '@/test/authHelper'
import type {
GetWorkspaceProjectsQuery,
ProjectUpdateRoleInput
} from '@/modules/core/graph/generated/graphql'
import {
ActiveUserProjectsDocument,
ActiveUserProjectsWorkspaceDocument,
CreateWorkspaceProjectDocument,
GetProjectDocument,
GetWorkspaceDocument,
GetWorkspaceProjectsDocument,
GetWorkspaceTeamDocument,
MoveProjectToWorkspaceDocument,
ProjectVisibility,
UpdateProjectDocument,
UpdateProjectRoleDocument,
UpdateWorkspaceProjectRoleDocument
} from '@/modules/core/graph/generated/graphql'
import type { ExecuteOperationResponse, TestApolloServer } from '@/test/graphqlHelper'
import { testApolloServer } from '@/test/graphqlHelper'
import { beforeEachContext } from '@/test/hooks'
import { mockAdminOverride } from '@/test/mocks/global'
import { isMultiRegionTestMode } from '@/test/speckle-helpers/regions'
import type { BasicTestStream } from '@/test/speckle-helpers/streamHelper'
import {
addToStream,
createTestStream,
getUserStreamRole
} from '@/test/speckle-helpers/streamHelper'
import type { Nullable, Optional } from '@speckle/shared'
import {
isNonNullable,
PaidWorkspacePlans,
Roles,
WorkspacePlans
} from '@speckle/shared'
import { expect } from 'chai'
import cryptoRandomString from 'crypto-random-string'
import dayjs from 'dayjs'
import type { Knex } from 'knex'
import { times } from 'lodash-es'
const grantStreamPermissions = grantStreamPermissionsFactory({ db })
const adminOverrideMock = mockAdminOverride()
const tables = {
streams: (db: Knex) => db.table<StreamRecord>(Streams.name)
}
describe('Workspace project GQL CRUD', () => {
let apollo: TestApolloServer
const workspace: BasicTestWorkspace = {
id: '',
ownerId: '',
slug: cryptoRandomString({ length: 10 }),
name: 'My Test Workspace'
}
const serverAdminUser: BasicTestUser = {
id: '',
name: 'John Speckle',
email: 'john-speckle-workspace-project-admin@example.org',
role: Roles.Server.Admin
}
const serverMemberUser: BasicTestUser = {
id: '',
name: 'John Nobody',
email: 'john-nobody@example.org',
role: Roles.Server.User
}
before(async () => {
await beforeEachContext()
await createTestUsers([serverAdminUser, serverMemberUser])
apollo = await testApolloServer({
authUserId: serverAdminUser.id
})
await createTestWorkspace(workspace, serverAdminUser)
const workspaceProjects = [
{ name: 'Workspace Project A', workspaceId: workspace.id },
{ name: 'Workspace Project B', workspaceId: workspace.id },
{ name: 'Workspace Project C', workspaceId: workspace.id }
]
await Promise.all(
workspaceProjects.map((input) =>
apollo.execute(CreateWorkspaceProjectDocument, { input })
)
)
})
describe('when creating project', () => {
it('should have workspace visibility by default', async () => {
const res = await apollo.execute(
CreateWorkspaceProjectDocument,
{
input: {
name: 'Test Default Project',
workspaceId: workspace.id
}
},
{ assertNoErrors: true }
)
const project = res.data?.workspaceMutations?.projects.create
expect(project).to.be.ok
expect(project?.visibility).to.equal(ProjectVisibility.Workspace)
})
it('should create the project in that workspace', async () => {
const projectName = cryptoRandomString({ length: 6 })
const createRes = await apollo.execute(CreateWorkspaceProjectDocument, {
input: {
name: projectName,
workspaceId: workspace.id
}
})
const getRes = await apollo.execute(GetWorkspaceProjectsDocument, {
id: workspace.id
})
const workspaceProject = getRes.data?.workspace.projects.items.find(
(project) => project.name === projectName
)
expect(createRes).to.not.haveGraphQLErrors()
expect(getRes).to.not.haveGraphQLErrors()
expect(workspaceProject).to.exist
})
})
describe('when changing workspace project roles', () => {
const workspaceGuest: BasicTestUser = {
id: '',
name: 'John Guest 2',
email: 'johnguest2@bababooey.com'
}
const workspaceEditor: BasicTestUser = {
id: '',
name: 'John Editor 2',
email: 'johneditor2@bababooey.com'
}
const workspaceMemberViewer: BasicTestUser = {
id: '',
name: 'John Member Viewer',
email: 'johnmemberviewer@bababooey.com'
}
before(async () => {
await Promise.all([
createTestUser(workspaceGuest),
createTestUser(workspaceEditor),
createTestUser(workspaceMemberViewer)
])
})
describe(`with pro plan`, () => {
const roleProject: BasicTestStream = {
name: 'Role Project',
isPublic: false,
id: '',
ownerId: ''
}
const roleWorkspace: BasicTestWorkspace = {
id: '',
ownerId: '',
slug: cryptoRandomString({ length: 10 }),
name: 'Role Workspace'
}
before(async () => {
// TODO: Multiregion
await createTestWorkspace(roleWorkspace, serverAdminUser, {
addPlan: { name: PaidWorkspacePlans.Pro, status: 'valid' }
})
roleProject.workspaceId = roleWorkspace.id
await Promise.all([
assignToWorkspace(roleWorkspace, workspaceGuest, Roles.Workspace.Guest),
assignToWorkspace(
roleWorkspace,
workspaceEditor,
Roles.Workspace.Member,
WorkspaceSeatType.Editor
),
assignToWorkspace(
roleWorkspace,
workspaceMemberViewer,
Roles.Workspace.Member,
WorkspaceSeatType.Viewer
)
])
await createTestStream(roleProject, serverAdminUser)
await Promise.all([
addToStream(roleProject, workspaceGuest, Roles.Stream.Reviewer),
addToStream(roleProject, workspaceEditor, Roles.Stream.Contributor),
addToStream(roleProject, workspaceMemberViewer, Roles.Stream.Reviewer)
])
// assert seat types
const seats = await getWorkspaceUserSeatsFactory({ db })({
workspaceId: roleWorkspace.id,
userIds: [workspaceGuest.id, workspaceEditor.id, workspaceMemberViewer.id]
})
expect(seats[workspaceGuest.id].type).to.equal(WorkspaceSeatType.Viewer)
expect(seats[workspaceEditor.id].type).to.equal(WorkspaceSeatType.Editor)
expect(seats[workspaceMemberViewer.id].type).to.equal(WorkspaceSeatType.Viewer)
})
describeEach(
[{ oldResolver: true }, { oldResolver: false }],
({ oldResolver }) => `with ${oldResolver ? 'old' : 'new'} updateRole resolver`,
({ oldResolver }) => {
const updateRole = async (input: ProjectUpdateRoleInput) => {
if (oldResolver) {
const res = await apollo.execute(UpdateProjectRoleDocument, {
input
})
const project = res.data?.projectMutations?.updateRole
return { res, project }
} else {
const res = await apollo.execute(UpdateWorkspaceProjectRoleDocument, {
input
})
const project = res.data?.workspaceMutations?.projects?.updateRole
return { res, project }
}
}
it("can't set a workspace guest as a project owner", async () => {
const { res } = await updateRole({
projectId: roleProject.id,
userId: workspaceGuest.id,
role: Roles.Stream.Owner
})
const newRole = await getUserStreamRole(workspaceGuest.id, roleProject.id)
expect(res).to.haveGraphQLErrors({ code: WorkspaceInvalidRoleError.code })
expect(newRole).to.eq(Roles.Stream.Reviewer)
})
it(`can not set a workspace viewer as a project contributor or owner`, async () => {
const { res: resA } = await updateRole({
projectId: roleProject.id,
userId: workspaceMemberViewer.id,
role: Roles.Stream.Contributor
})
const { res: resB } = await updateRole({
projectId: roleProject.id,
userId: workspaceMemberViewer.id,
role: Roles.Stream.Owner
})
const newRole = await getUserStreamRole(
workspaceMemberViewer.id,
roleProject.id
)
expect(resA).to.haveGraphQLErrors({
code: WorkspaceInvalidRoleError.code
})
expect(resB).to.haveGraphQLErrors({
code: WorkspaceInvalidRoleError.code
})
expect(newRole).to.eq(Roles.Stream.Reviewer)
})
}
)
})
})
describe('when querying projects', () => {
const PAGE_SIZE = 5
const PAGE_COUNT = 3
const TOTAL_WS_PROJECT_COUNT = PAGE_COUNT * PAGE_SIZE
const GUEST_PROJECT_COUNT = PAGE_SIZE + 1
const NON_WORKSPACE_PROJECT_COUNT = 5
const queryWorkspace: BasicTestWorkspace = {
id: '',
ownerId: '',
slug: '',
name: 'Query Workspace'
}
const workspaceGuest: BasicTestUser = {
id: '',
email: '',
name: 'Query Workspace Guest'
}
const workspaceAdmin = serverMemberUser
const workspaceAdmin2: BasicTestUser = {
id: '',
email: '',
name: 'Query Workspace Admin 2'
}
const workspaceMember: BasicTestUser = {
id: '',
email: '',
name: 'Query Workspace Member'
}
const workspaceMemberNoExplicitRoles: BasicTestUser = {
id: '',
email: '',
name: 'Query Workspace Member w/ No Explicit Project Roles'
}
let wsProjects: BasicTestStream[]
let nonWorkspaceProjects: BasicTestStream[]
let apollo: TestApolloServer
before(async () => {
await createTestUsers([
workspaceGuest,
workspaceMember,
workspaceAdmin2,
workspaceMemberNoExplicitRoles
])
await createTestWorkspace(queryWorkspace, workspaceAdmin, {
addPlan: { name: 'team', status: 'valid' }
})
await assignToWorkspaces([
[
queryWorkspace,
workspaceGuest,
Roles.Workspace.Guest,
WorkspaceSeatType.Editor
],
[
queryWorkspace,
workspaceMember,
Roles.Workspace.Member,
WorkspaceSeatType.Editor
],
[
queryWorkspace,
workspaceAdmin2,
Roles.Workspace.Admin,
WorkspaceSeatType.Editor
],
[
queryWorkspace,
workspaceMemberNoExplicitRoles,
Roles.Workspace.Member,
WorkspaceSeatType.Editor
]
])
wsProjects = times(
TOTAL_WS_PROJECT_COUNT,
(i): BasicTestStream => ({
id: '',
ownerId: '',
name: `Query Workspace Project - #${i}`,
// Make all except the very last one workspace visibility
visibility:
i === TOTAL_WS_PROJECT_COUNT - 1
? ProjectRecordVisibility.Private
: ProjectRecordVisibility.Workspace,
workspaceId: queryWorkspace.id
})
)
nonWorkspaceProjects = times(
NON_WORKSPACE_PROJECT_COUNT,
(i): BasicTestStream => ({
id: '',
ownerId: '',
name: `Non Workspace Project - #${i}`,
visibility: ProjectRecordVisibility.Private
})
)
// CREATE CONCURRENTLY TO TEST COMPOSITE CURSOR (same updatedAt)
await Promise.all([
...wsProjects.map((project) => createTestStream(project, workspaceAdmin)),
...nonWorkspaceProjects.map((project) =>
createTestStream(project, workspaceGuest)
)
])
// ONLY ADD EXPLICIT PROJECT ASSIGNMENTS TO GUEST
const projectsToAssign = wsProjects.slice(0, GUEST_PROJECT_COUNT)
await Promise.all(
projectsToAssign.map((project) =>
addToStream(project, workspaceGuest, Roles.Stream.Contributor)
)
)
await Promise.all([
// Add explicit single assignment to workspaceMember & workspaceAdmin to 1st non-workspace project
addToStream(nonWorkspaceProjects[0], workspaceMember, Roles.Stream.Contributor),
addToStream(nonWorkspaceProjects[0], workspaceAdmin, Roles.Stream.Contributor),
// Add explicit single assignment to workspaceMember to 1st workspace project
addToStream(wsProjects[0], workspaceMember, Roles.Stream.Contributor)
])
apollo = await testApolloServer({
authUserId: workspaceAdmin.id
})
})
// projects at the end have no explicit project assignments (and very last one is fully private),
// and first X ones are explicitly assigned to guest user
const implicitPrivateProject = () => wsProjects.at(-1)!
const implicitWorkspaceVisibilityProject = () => wsProjects.at(-2)!
const explicitGuestProject = () => wsProjects.at(0)!
afterEach(async () => {
adminOverrideMock.disable()
})
describe('through Workspace.projects', () => {
it('should return all projects for workspace admin', async () => {
const res = await apollo.execute(GetWorkspaceProjectsDocument, {
id: queryWorkspace.id,
limit: 999 // get everything
})
expect(res).to.not.haveGraphQLErrors()
const collection = res.data?.workspace.projects
expect(collection?.items.length).to.equal(TOTAL_WS_PROJECT_COUNT)
expect(collection?.cursor).to.be.ok
expect(collection?.totalCount).to.eq(TOTAL_WS_PROJECT_COUNT)
// validate sorting
const projects = collection?.items || []
let lastUpdatedAt: Optional<Date> = undefined
for (const project of projects) {
const date = project.updatedAt
if (!lastUpdatedAt) {
lastUpdatedAt = date
continue
}
expect(
dayjs(date).isSame(dayjs(lastUpdatedAt)) ||
dayjs(date).isBefore(dayjs(lastUpdatedAt))
).to.be.true
lastUpdatedAt = date
}
})
itEach(
[{ adminOverrideEnabled: true }, { adminOverrideEnabled: false }],
({ adminOverrideEnabled }) =>
adminOverrideEnabled
? 'should return all projects for server admins if override enabled'
: 'should fail retrieving projects for server admins if no override enabled',
async ({ adminOverrideEnabled }) => {
const apollo = await testApolloServer({
authUserId: serverAdminUser.id
})
adminOverrideMock.enable(adminOverrideEnabled)
const res = await apollo.execute(GetWorkspaceProjectsDocument, {
id: queryWorkspace.id,
limit: 999 // get everything
})
if (adminOverrideEnabled) {
expect(res).to.not.haveGraphQLErrors()
const collection = res.data?.workspace.projects
expect(collection?.items.length).to.equal(TOTAL_WS_PROJECT_COUNT)
expect(collection?.cursor).to.be.ok
expect(collection?.totalCount).to.eq(TOTAL_WS_PROJECT_COUNT)
} else {
expect(res).to.haveGraphQLErrors()
const collection = res.data?.workspace.projects
expect(collection).to.not.be.ok
}
}
)
it('should return only explicitly assigned projects for guests', async () => {
const apollo = await testApolloServer({
authUserId: workspaceGuest.id
})
const res = await apollo.execute(GetWorkspaceProjectsDocument, {
id: queryWorkspace.id,
limit: 999 // get everything
})
expect(res).to.not.haveGraphQLErrors()
const collection = res.data?.workspace.projects
expect(collection?.items.length).to.equal(GUEST_PROJECT_COUNT)
expect(collection?.cursor).to.be.ok
expect(collection?.totalCount).to.equal(GUEST_PROJECT_COUNT)
})
it('should return all non-private for members who may not even have any explicit project roles', async () => {
const apollo = await testApolloServer({
authUserId: workspaceMemberNoExplicitRoles.id
})
const res = await apollo.execute(GetWorkspaceProjectsDocument, {
id: queryWorkspace.id,
limit: 999 // get everything
})
const nonPrivateCount = TOTAL_WS_PROJECT_COUNT - 1 // -1 for the fully private one
expect(res).to.not.haveGraphQLErrors()
const collection = res.data?.workspace.projects
expect(collection?.items.length).to.equal(nonPrivateCount)
expect(collection?.cursor).to.be.ok
expect(collection?.totalCount).to.equal(nonPrivateCount)
})
it('should respect limits', async () => {
const res = await apollo.execute(GetWorkspaceProjectsDocument, {
id: queryWorkspace.id,
limit: 1
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.workspace.projects.items.length).to.equal(1)
expect(res.data?.workspace.projects.cursor).to.be.ok
expect(res.data?.workspace.projects.totalCount).to.equal(TOTAL_WS_PROJECT_COUNT)
})
it('should only return totalCount if limit === 0', async () => {
const res = await apollo.execute(GetWorkspaceProjectsDocument, {
id: queryWorkspace.id,
limit: 0
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.workspace.projects.items.length).to.equal(0)
expect(res.data?.workspace.projects.cursor).to.be.null
expect(res.data?.workspace.projects.totalCount).to.equal(TOTAL_WS_PROJECT_COUNT)
})
it('should respect pagination', async () => {
let newCursor: Nullable<string> = null
for (let page = 1; page <= PAGE_COUNT + 1; page++) {
const res: ExecuteOperationResponse<GetWorkspaceProjectsQuery> =
await apollo.execute(GetWorkspaceProjectsDocument, {
id: queryWorkspace.id,
limit: PAGE_SIZE,
cursor: newCursor
})
newCursor = res.data?.workspace.projects.cursor || null
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.workspace.projects.totalCount).to.equal(
TOTAL_WS_PROJECT_COUNT
)
if (page <= PAGE_COUNT) {
expect(res.data?.workspace.projects.items.length).to.equal(PAGE_SIZE)
expect(res.data?.workspace.projects.cursor).to.be.ok
} else {
expect(res.data?.workspace.projects.items.length).to.eq(0)
expect(res.data?.workspace.projects.cursor).to.be.null
}
}
})
it('should respect search filters', async () => {
const res = await apollo.execute(GetWorkspaceProjectsDocument, {
id: queryWorkspace.id,
filter: {
search: 'Query Workspace Project - #0'
}
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.workspace.projects.items.length).to.equal(1)
expect(res.data?.workspace.projects.totalCount).to.equal(1)
expect(res.data?.workspace.projects.cursor).to.be.ok
const project = res.data?.workspace.projects.items[0]
expect(project).to.exist
expect(project?.name).to.equal('Query Workspace Project - #0')
})
it('should respect withProjectRoleOnly flag', async () => {
const apollo = await testApolloServer({
authUserId: workspaceMember.id
})
const res = await apollo.execute(GetWorkspaceProjectsDocument, {
id: queryWorkspace.id,
filter: {
withProjectRoleOnly: true
}
})
expect(res).to.not.haveGraphQLErrors()
const collection = res.data?.workspace.projects
expect(collection).to.be.ok
expect(collection?.items.length).to.equal(1)
expect(collection?.items[0].id).to.equal(wsProjects[0].id)
expect(collection?.totalCount).to.equal(1)
})
})
describe('for a specific one', () => {
const randomServerGuy: BasicTestUser = {
id: '',
name: 'Random Server Guy',
email: ''
}
before(async () => {
await createTestUser(randomServerGuy)
})
it('workspace visibility should be accessible to workspace member', async () => {
const apollo = await testApolloServer({
authUserId: workspaceMember.id
})
const res = await apollo.execute(GetProjectDocument, {
id: implicitWorkspaceVisibilityProject().id
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.project.id).to.be.ok
})
it('private visibility should not be accessible to workspace member w/o explicit role', async () => {
const apollo = await testApolloServer({
authUserId: workspaceMember.id
})
const res = await apollo.execute(GetProjectDocument, {
id: implicitPrivateProject().id
})
expect(res).to.haveGraphQLErrors()
expect(res.data?.project).to.not.be.ok
})
it('private visibility should be accessible to workspace admin w/o explicit role', async () => {
const apollo = await testApolloServer({
authUserId: workspaceAdmin2.id
})
const res = await apollo.execute(GetProjectDocument, {
id: implicitPrivateProject().id
})
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.project.id).to.be.ok
})
it('it should not be accessible to random outside workspace guy', async () => {
const apollo = await testApolloServer({
authUserId: randomServerGuy.id
})
const res = await apollo.execute(GetProjectDocument, {
id: implicitWorkspaceVisibilityProject().id
})
expect(res).to.haveGraphQLErrors()
expect(res.data?.project).to.not.be.ok
})
itEach(
[{ explicit: false }, { explicit: true }],
({ explicit }) =>
explicit
? 'it should be accessible to workspace guest with explicit project role'
: 'it should not be accessible to workspace guest without explicit project role',
async ({ explicit }) => {
const apollo = await testApolloServer({
authUserId: workspaceGuest.id
})
const res = await apollo.execute(GetProjectDocument, {
id: explicit
? explicitGuestProject().id
: implicitWorkspaceVisibilityProject().id
})
if (explicit) {
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.project.id).to.be.ok
} else {
expect(res).to.haveGraphQLErrors()
expect(res.data?.project).to.not.be.ok
}
}
)
itEach(
[{ adminOverrideEnabled: true }, { adminOverrideEnabled: false }],
({ adminOverrideEnabled }) =>
adminOverrideEnabled
? 'it should return fully private project for server admins if override enabled'
: 'it should not return fully private project for server admins if override disabled',
async ({ adminOverrideEnabled }) => {
const apollo = await testApolloServer({
authUserId: serverAdminUser.id
})
adminOverrideMock.enable(adminOverrideEnabled)
const res = await apollo.execute(GetProjectDocument, {
id: implicitPrivateProject().id
})
if (adminOverrideEnabled) {
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.project.id).to.be.ok
} else {
expect(res).to.haveGraphQLErrors()
expect(res.data?.project).to.not.be.ok
}
}
)
})
describe('through ActiveUser.projects', () => {
let apollo: TestApolloServer
before(async () => {
apollo = await testApolloServer({
authUserId: workspaceGuest.id
})
})
it('should return all projects user is explicitly assigned to', async () => {
// guest
const apolloGuest = await testApolloServer({
authUserId: workspaceGuest.id
})
const guestRes = await apolloGuest.execute(
ActiveUserProjectsWorkspaceDocument,
{ limit: 999 },
{ assertNoErrors: true }
)
const guestCollection = guestRes.data?.activeUser?.projects
const expectedGuestCount = GUEST_PROJECT_COUNT + NON_WORKSPACE_PROJECT_COUNT
expect(guestCollection).to.be.ok
expect(guestCollection!.totalCount).to.equal(expectedGuestCount)
expect(guestCollection!.items.length).to.equal(expectedGuestCount)
expect(
guestCollection!.items.map((i) => i.workspace?.id).filter(isNonNullable)
).to.have.length(GUEST_PROJECT_COUNT)
// member
const apolloMember = await testApolloServer({
authUserId: workspaceMember.id
})
const memberRes = await apolloMember.execute(
ActiveUserProjectsWorkspaceDocument,
{ limit: 999 },
{ assertNoErrors: true }
)
const memberCollection = memberRes.data?.activeUser?.projects
const expectedMemberCount = 2 // only 2 explicit assignments
expect(memberCollection).to.be.ok
expect(memberCollection!.totalCount).to.equal(expectedMemberCount)
expect(memberCollection!.items.length).to.equal(expectedMemberCount)
expect([
memberCollection!.items[0].id,
memberCollection!.items[1].id
]).to.deep.equalInAnyOrder([nonWorkspaceProjects[0].id, wsProjects[0].id])
})
itEach(
[{ admin: true }, { admin: false }],
({ admin }) =>
`should return all projects ${
admin ? 'ws admin' : 'ws member'
} is explicitly or implicitly assigned to, if flag set`,
async ({ admin }) => {
const apollo = await testApolloServer({
authUserId: admin ? workspaceAdmin.id : workspaceMember.id
})
const res = await apollo.execute(
ActiveUserProjectsWorkspaceDocument,
{ limit: 999, filter: { includeImplicitAccess: true } },
{ assertNoErrors: true }
)
const projects = res.data?.activeUser?.projects
// 1 non-workspace assignment + all workspace projects
// (except the last one thats fully private, if not admin)
let expectedCount = TOTAL_WS_PROJECT_COUNT + 1
if (!admin) {
expectedCount -= 1
}
expect(projects).to.be.ok
expect(projects!.totalCount).to.equal(expectedCount)
expect(projects!.items.length).to.equal(expectedCount)
expect(projects!.items.map((i) => i.id)).to.deep.equalInAnyOrder([
nonWorkspaceProjects[0].id,
...wsProjects
.filter((p) => (admin ? true : p.id !== implicitPrivateProject().id))
.map((p) => p.id)
])
}
)
it('should only return workspace projects if filter set', async () => {
const res = await apollo.execute(ActiveUserProjectsWorkspaceDocument, {
filter: {
workspaceId: queryWorkspace.id
},
limit: 999
})
const expectedCount = GUEST_PROJECT_COUNT
expect(res).to.not.haveGraphQLErrors()
const collection = res.data?.activeUser?.projects
expect(collection).to.be.ok
expect(collection?.items.length).to.equal(expectedCount)
expect(collection?.totalCount).to.equal(expectedCount)
expect(
collection?.items.map((i) => i.workspace?.id).filter(isNonNullable)
).to.have.length(expectedCount)
})
it('should only return non-workspace projects if filter set', async () => {
const res = await apollo.execute(ActiveUserProjectsWorkspaceDocument, {
filter: {
personalOnly: true
},
limit: 999
})
const expectedCount = NON_WORKSPACE_PROJECT_COUNT
expect(res).to.not.haveGraphQLErrors()
const collection = res.data?.activeUser?.projects
expect(collection).to.be.ok
expect(collection?.items.length).to.equal(expectedCount)
expect(collection?.totalCount).to.equal(expectedCount)
expect(
collection?.items.map((i) => i.workspace?.id).filter((v) => !v)
).to.have.length(expectedCount)
})
})
})
describe('when moving a project to a workspace', () => {
const testProject: BasicTestStream = {
id: '',
ownerId: '',
name: 'Test Project',
visibility: ProjectRecordVisibility.Private
}
const targetWorkspace: BasicTestWorkspace = {
id: '',
ownerId: '',
slug: cryptoRandomString({ length: 10 }),
name: 'Target Workspace'
}
before(async () => {
await createTestWorkspace(targetWorkspace, serverAdminUser, {
addPlan: WorkspacePlans.Unlimited
})
})
beforeEach(async () => {
await createTestStream(testProject, serverAdminUser)
await grantStreamPermissions({
streamId: testProject.id,
userId: serverMemberUser.id,
role: Roles.Stream.Contributor
})
})
it('should move the project to the target workspace and update visibility', async () => {
const res = await apollo.execute(MoveProjectToWorkspaceDocument, {
projectId: testProject.id,
workspaceId: targetWorkspace.id
})
const project = res.data?.workspaceMutations.projects.moveToWorkspace
expect(res).to.not.haveGraphQLErrors()
expect(project?.workspaceId).to.equal(targetWorkspace.id)
expect(project?.visibility).to.equal(ProjectVisibility.Workspace)
})
it('should move a public project to the target workspace and keep same visibility', async () => {
const publicProject: BasicTestStream = {
id: '',
ownerId: '',
name: 'Test Public Project',
visibility: ProjectRecordVisibility.Public
}
await createTestStream(publicProject, serverAdminUser)
const res = await apollo.execute(MoveProjectToWorkspaceDocument, {
projectId: publicProject.id,
workspaceId: targetWorkspace.id
})
const project = res.data?.workspaceMutations.projects.moveToWorkspace
expect(res).to.not.haveGraphQLErrors()
expect(project?.workspaceId).to.equal(targetWorkspace.id)
expect(project?.visibility).to.equal(ProjectVisibility.Public)
})
it('should preserve project roles for project members with editor seats', async () => {
const res = await apollo.execute(MoveProjectToWorkspaceDocument, {
projectId: testProject.id,
workspaceId: targetWorkspace.id
})
const { team } = res.data?.workspaceMutations.projects.moveToWorkspace ?? {}
const adminProjectRole = team?.find((role) => role.id === serverAdminUser.id)
expect(res).to.not.haveGraphQLErrors()
expect(adminProjectRole?.role).to.equal(Roles.Stream.Owner)
})
it('should demote users with editor project roles and workspace viewer seats', async () => {
const res = await apollo.execute(MoveProjectToWorkspaceDocument, {
projectId: testProject.id,
workspaceId: targetWorkspace.id
})
const { team } = res.data?.workspaceMutations.projects.moveToWorkspace ?? {}
const memberProjectRole = team?.find((role) => role.id === serverMemberUser.id)
expect(res).to.not.haveGraphQLErrors()
expect(memberProjectRole?.role).to.equal(Roles.Stream.Reviewer)
})
it('should grant workspace roles to project members that are not already in the target workspace', async () => {
const resA = await apollo.execute(MoveProjectToWorkspaceDocument, {
projectId: testProject.id,
workspaceId: targetWorkspace.id
})
const resB = await apollo.execute(GetWorkspaceTeamDocument, {
workspaceId: targetWorkspace.id
})
const memberWorkspaceRole = resB.data?.workspace.team.items.find(
(role) => role.id === serverMemberUser.id
)
expect(resA).to.not.haveGraphQLErrors()
expect(resB).to.not.haveGraphQLErrors()
expect(memberWorkspaceRole?.role).to.equal(Roles.Workspace.Member)
})
it('should preserve workspace roles for project members that are already in the target workspace', async () => {
const resA = await apollo.execute(MoveProjectToWorkspaceDocument, {
projectId: testProject.id,
workspaceId: targetWorkspace.id
})
const resB = await apollo.execute(GetWorkspaceTeamDocument, {
workspaceId: targetWorkspace.id
})
const adminWorkspaceRole = resB.data?.workspace.team.items.find(
(role) => role.id === serverAdminUser.id
)
expect(resA).to.not.haveGraphQLErrors()
expect(resB).to.not.haveGraphQLErrors()
expect(adminWorkspaceRole?.role).to.equal(Roles.Workspace.Admin)
})
isMultiRegionTestMode()
? describe('when the default server db region is not the main db @multiregion', () => {
let regionalProject: BasicTestStream
before(async () => {
// Simulate non-main default db region
regionalProject = await createTestStream(
{
name: 'My Special Project',
description: null,
clonedFrom: null,
createdAt: new Date(),
updatedAt: new Date(),
allowPublicComments: false,
workspaceId: null,
regionKey: 'region1',
visibility: ProjectRecordVisibility.Public
},
serverAdminUser
)
})
it('should be located in the correct region', async () => {
const regionDb = await getRegionDb({ regionKey: 'region1' })
const [res] = await tables
.streams(regionDb)
.where({ id: regionalProject.id })
expect(res).to.exist
})
it('should update project without removing workspace association @multiregion', async () => {
const resA = await apollo.execute(MoveProjectToWorkspaceDocument, {
projectId: regionalProject.id,
workspaceId: targetWorkspace.id
})
const resB = await apollo.execute(UpdateProjectDocument, {
input: {
id: regionalProject.id,
name: 'Foo'
}
})
const resC = await apollo.execute(GetProjectDocument, {
id: regionalProject.id
})
expect(resA).to.not.haveGraphQLErrors()
expect(resB).to.not.haveGraphQLErrors()
expect(resC).to.not.haveGraphQLErrors()
expect(resC.data?.project?.workspaceId).to.equal(targetWorkspace.id)
})
})
: null
})
// moved over Alessandro's tests from core to here, since they are all related to workspaces
// they're kind of a mess and need to be cleaned up
describe('query user.projects', () => {
it('should return projects not in a workspace', async () => {
const testAdminUser: BasicTestUser = {
id: '',
name: 'test',
email: '',
role: Roles.Server.Admin,
verified: true
}
await createTestUser(testAdminUser)
const workspace = {
id: '',
name: 'test ws',
slug: cryptoRandomString({ length: 10 }),
ownerId: ''
}
await createTestWorkspace(workspace, testAdminUser)
const session = await login(testAdminUser)
const getWorkspaceRes = await session.execute(GetWorkspaceDocument, {
workspaceId: workspace.id
})
expect(getWorkspaceRes).to.not.haveGraphQLErrors()
const workspaceId = getWorkspaceRes.data!.workspace.id
const createProjectInWorkspaceRes = await session.execute(
CreateWorkspaceProjectDocument,
{ input: { name: 'project', workspaceId } }
)
expect(createProjectInWorkspaceRes).to.not.haveGraphQLErrors()
// create w/o GQL, to not mess w/ personal project limits
const projectNonInWorkspace: BasicTestStream = {
id: '',
name: 'project',
ownerId: '',
isPublic: false
}
await createTestStream(projectNonInWorkspace, testAdminUser)
const userProjectsRes = await session.execute(ActiveUserProjectsDocument, {
filter: { personalOnly: true }
})
expect(userProjectsRes).to.not.haveGraphQLErrors()
const projects = userProjectsRes.data!.activeUser!.projects.items
expect(projects).to.have.length(1)
expect(projects[0].id).to.eq(projectNonInWorkspace.id)
})
it('should return projects in workspace', async () => {
const testAdminUser: BasicTestUser = {
id: '',
name: 'test',
email: '',
role: Roles.Server.Admin,
verified: true
}
await createTestUser(testAdminUser)
const workspace = {
id: '',
name: 'test ws',
slug: cryptoRandomString({ length: 10 }),
ownerId: ''
}
await createTestWorkspace(workspace, testAdminUser)
const session = await login(testAdminUser)
const getWorkspaceRes = await session.execute(GetWorkspaceDocument, {
workspaceId: workspace.id
})
expect(getWorkspaceRes).to.not.haveGraphQLErrors()
const workspaceId = getWorkspaceRes.data!.workspace.id
const createProjectInWorkspaceRes = await session.execute(
CreateWorkspaceProjectDocument,
{ input: { name: 'project', workspaceId } }
)
expect(createProjectInWorkspaceRes).to.not.haveGraphQLErrors()
const projectInWorkspace =
createProjectInWorkspaceRes.data!.workspaceMutations.projects.create
// create w/o GQL, to not mess w/ personal project limits
const projectNonInWorkspace: BasicTestStream = {
id: '',
name: 'project',
ownerId: '',
isPublic: false
}
await createTestStream(projectNonInWorkspace, testAdminUser)
const userProjectsRes = await session.execute(ActiveUserProjectsDocument, {
filter: { workspaceId }
})
expect(userProjectsRes).to.not.haveGraphQLErrors()
const projects = userProjectsRes.data!.activeUser!.projects.items
expect(projects).to.have.length(1)
expect(projects[0].id).to.eq(projectInWorkspace.id)
})
it('should return all user projects', async () => {
const testAdminUser: BasicTestUser = {
id: '',
name: 'test',
email: '',
role: Roles.Server.Admin,
verified: true
}
await createTestUser(testAdminUser)
const workspace = {
id: '',
name: 'test ws',
slug: cryptoRandomString({ length: 10 }),
ownerId: ''
}
await createTestWorkspace(workspace, testAdminUser)
const session = await login(testAdminUser)
const getWorkspaceRes = await session.execute(GetWorkspaceDocument, {
workspaceId: workspace.id
})
expect(getWorkspaceRes).to.not.haveGraphQLErrors()
const workspaceId = getWorkspaceRes.data!.workspace.id
const createProjectInWorkspaceRes = await session.execute(
CreateWorkspaceProjectDocument,
{ input: { name: 'project', workspaceId } }
)
expect(createProjectInWorkspaceRes).to.not.haveGraphQLErrors()
// create w/o GQL, to not mess w/ personal project limits
const projectNonInWorkspace: BasicTestStream = {
id: '',
name: 'project',
ownerId: '',
isPublic: false
}
await createTestStream(projectNonInWorkspace, testAdminUser)
const userProjectsRes = await session.execute(ActiveUserProjectsDocument, {
filter: {}
})
expect(userProjectsRes).to.not.haveGraphQLErrors()
const projects = userProjectsRes.data!.activeUser!.projects.items
expect(projects).to.have.length(2)
})
it('should return all user projects sorted by user role', async () => {
const testAdminUser: BasicTestUser = {
id: '',
name: 'test',
email: '',
role: Roles.Server.Admin,
verified: true
}
await createTestUser(testAdminUser)
const workspace = {
id: '',
name: 'test ws',
slug: cryptoRandomString({ length: 10 }),
ownerId: ''
}
await createTestWorkspace(workspace, testAdminUser)
const session = await login(testAdminUser)
const getWorkspaceRes = await session.execute(GetWorkspaceDocument, {
workspaceId: workspace.id
})
expect(getWorkspaceRes).to.not.haveGraphQLErrors()
const workspaceId = getWorkspaceRes.data!.workspace.id
const createProjectInWorkspaceAsOwnerRes = await session.execute(
CreateWorkspaceProjectDocument,
{ input: { name: 'project', workspaceId } }
)
expect(createProjectInWorkspaceAsOwnerRes).to.not.haveGraphQLErrors()
const createProjectInWorkspaceAsContributorRes = await session.execute(
CreateWorkspaceProjectDocument,
{ input: { name: 'project 2', workspaceId } }
)
expect(createProjectInWorkspaceAsContributorRes).to.not.haveGraphQLErrors()
const projectContributorId =
createProjectInWorkspaceAsContributorRes.data?.workspaceMutations.projects
.create.id
await db(StreamAcl.name)
.update({ role: Roles.Stream.Contributor })
.where({ userId: testAdminUser.id, resourceId: projectContributorId })
const createProjectInWorkspaceAsReviewerRes = await session.execute(
CreateWorkspaceProjectDocument,
{ input: { name: 'project 3', workspaceId } }
)
expect(createProjectInWorkspaceAsReviewerRes).to.not.haveGraphQLErrors()
const projectReviewerId =
createProjectInWorkspaceAsReviewerRes.data?.workspaceMutations.projects.create
.id
await db(StreamAcl.name)
.update({ role: Roles.Stream.Reviewer })
.where({ userId: testAdminUser.id, resourceId: projectReviewerId })
const userProjectsRes = await session.execute(ActiveUserProjectsDocument, {
filter: {},
sortBy: ['role']
})
expect(userProjectsRes).to.not.haveGraphQLErrors()
const projects = userProjectsRes.data!.activeUser!.projects.items
expect(projects).to.have.length(3)
expect(projects[0].id).to.eq(
createProjectInWorkspaceAsOwnerRes.data?.workspaceMutations.projects.create.id
)
expect(projects[1].id).to.eq(projectContributorId)
expect(projects[2].id).to.eq(projectReviewerId)
})
})
})