diff --git a/packages/server/modules/workspaces/errors/workspace.ts b/packages/server/modules/workspaces/errors/workspace.ts index 7a734d45f..e0df38a5a 100644 --- a/packages/server/modules/workspaces/errors/workspace.ts +++ b/packages/server/modules/workspaces/errors/workspace.ts @@ -9,16 +9,25 @@ export class WorkspaceAdminRequiredError extends BaseError { export class WorkspaceInvalidRoleError extends BaseError { static defaultMessage = 'Invalid workspace role provided' static code = 'WORKSPACE_INVALID_ROLE_ERROR' + static statusCode = 400 } export class WorkspaceInvalidLogoError extends BaseError { static defaultMessage = 'Provided logo is not valid' static code = 'WORKSPACE_INVALID_LOGO_ERROR' + static statusCode = 400 } export class WorkspaceInvalidDescriptionError extends BaseError { static defaultMessage = 'Provided description is too long' static code = 'WORKSPACE_INVALID_DESCRIPTION_ERROR' + static statusCode = 400 +} + +export class WorkspaceNoVerifiedDomainsError extends BaseError { + static defaultMessage = 'Invalid operation, the workspace has no verified domains' + static code = 'WORKSPACE_NO_VERIFIED_DOMAINS' + static statusCode = 400 } export class WorkspaceQueryError extends BaseError { diff --git a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts index 787630e2c..57b95c2e8 100644 --- a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts +++ b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts @@ -315,7 +315,7 @@ export = FF_WORKSPACES_MODULE_ENABLED ) const updateWorkspace = updateWorkspaceFactory({ - getWorkspace: getWorkspaceFactory({ db }), + getWorkspace: getWorkspaceWithDomainsFactory({ db }), upsertWorkspace: upsertWorkspaceFactory({ db }), emitWorkspaceEvent: getEventBus().emit }) diff --git a/packages/server/modules/workspaces/services/management.ts b/packages/server/modules/workspaces/services/management.ts index 33cd446bd..dccf18db5 100644 --- a/packages/server/modules/workspaces/services/management.ts +++ b/packages/server/modules/workspaces/services/management.ts @@ -30,7 +30,8 @@ import { WorkspaceNotFoundError, WorkspaceProtectedError, WorkspaceUnverifiedDomainError, - WorkspaceInvalidDescriptionError + WorkspaceInvalidDescriptionError, + WorkspaceNoVerifiedDomainsError } from '@/modules/workspaces/errors/workspace' import { isUserLastWorkspaceAdmin } from '@/modules/workspaces/helpers/roles' import { EventBus } from '@/modules/shared/services/eventBus' @@ -50,7 +51,7 @@ import { blockedDomains } from '@/modules/workspaces/helpers/blockedDomains' import { DeleteAllResourceInvites } from '@/modules/serverinvites/domain/operations' import { WorkspaceInviteResourceType } from '@/modules/workspaces/domain/constants' import { ProjectInviteResourceType } from '@/modules/serverinvites/domain/constants' -import { chunk, isEmpty } from 'lodash' +import { chunk, isEmpty, omit } from 'lodash' import { DeleteProjectRole, UpsertProjectRole @@ -137,13 +138,13 @@ export const updateWorkspaceFactory = upsertWorkspace, emitWorkspaceEvent }: { - getWorkspace: GetWorkspace + getWorkspace: GetWorkspaceWithDomains upsertWorkspace: UpsertWorkspace emitWorkspaceEvent: EventBus['emit'] }) => async ({ workspaceId, workspaceInput }: WorkspaceUpdateArgs): Promise => { // Get existing workspace to merge with incoming changes - const currentWorkspace = await getWorkspace({ workspaceId }) + const currentWorkspace = await getWorkspace({ id: workspaceId }) if (!currentWorkspace) { throw new WorkspaceNotFoundError() } @@ -160,8 +161,15 @@ export const updateWorkspaceFactory = throw new WorkspaceInvalidDescriptionError() } + if ( + workspaceInput.discoverabilityEnabled && + !currentWorkspace.discoverabilityEnabled && + !currentWorkspace.domains.find((domain) => domain.verified) + ) + throw new WorkspaceNoVerifiedDomainsError() + const workspace = { - ...currentWorkspace, + ...omit(currentWorkspace, 'domains'), ...removeNullOrUndefinedKeys(workspaceInput), updatedAt: new Date() } diff --git a/packages/server/modules/workspaces/tests/helpers/creation.ts b/packages/server/modules/workspaces/tests/helpers/creation.ts index fdaa8adaf..01b77e63c 100644 --- a/packages/server/modules/workspaces/tests/helpers/creation.ts +++ b/packages/server/modules/workspaces/tests/helpers/creation.ts @@ -78,7 +78,7 @@ export const createTestWorkspace = async ( workspace.id = newWorkspace.id if (workspace.discoverabilityEnabled) { const updateWorkspace = updateWorkspaceFactory({ - getWorkspace: getWorkspaceFactory({ db }), + getWorkspace: getWorkspaceWithDomainsFactory({ db }), upsertWorkspace: upsertWorkspaceFactory({ db }), emitWorkspaceEvent: (...args) => getEventBus().emit(...args) }) @@ -93,7 +93,7 @@ export const createTestWorkspace = async ( if (workspace.domainBasedMembershipProtectionEnabled) { await updateWorkspaceFactory({ - getWorkspace: getWorkspaceFactory({ db }), + getWorkspace: getWorkspaceWithDomainsFactory({ db }), upsertWorkspace: upsertWorkspaceFactory({ db }), emitWorkspaceEvent: getEventBus().emit })({ diff --git a/packages/server/modules/workspaces/tests/unit/services/management.spec.ts b/packages/server/modules/workspaces/tests/unit/services/management.spec.ts index 05421e8aa..f6a210fa3 100644 --- a/packages/server/modules/workspaces/tests/unit/services/management.spec.ts +++ b/packages/server/modules/workspaces/tests/unit/services/management.spec.ts @@ -1,12 +1,14 @@ import { Workspace, WorkspaceAcl, - WorkspaceDomain + WorkspaceDomain, + WorkspaceWithDomains } from '@/modules/workspacesCore/domain/types' import { addDomainToWorkspaceFactory, createWorkspaceFactory, deleteWorkspaceRoleFactory, + updateWorkspaceFactory, updateWorkspaceRoleFactory } from '@/modules/workspaces/services/management' import { Roles } from '@speckle/shared' @@ -19,11 +21,13 @@ import { createRandomPassword } from '@/modules/core/helpers/testHelpers' import { WorkspaceAdminRequiredError, WorkspaceDomainBlockedError, + WorkspaceNotFoundError, + WorkspaceNoVerifiedDomainsError, WorkspaceProtectedError, WorkspaceUnverifiedDomainError } from '@/modules/workspaces/errors/workspace' import { UserEmail } from '@/modules/core/domain/userEmails/types' -import { omit } from 'lodash' +import { merge, omit } from 'lodash' import { GetWorkspaceWithDomains } from '@/modules/workspaces/domain/operations' import { FindVerifiedEmailsByUserId } from '@/modules/core/domain/userEmails/operations' import { EventNames } from '@/modules/shared/services/eventBus' @@ -137,6 +141,163 @@ describe('Workspace services', () => { }) }) }) + describe('updateWorkspaceFactory creates a function, that', () => { + const createTestWorkspaceWithDomainsData = ( + input: Partial = {} + ): WorkspaceWithDomains => { + const workspaceId = cryptoRandomString({ length: 10 }) + const workspace: WorkspaceWithDomains = { + id: workspaceId, + name: cryptoRandomString({ length: 10 }), + description: cryptoRandomString({ length: 20 }), + createdAt: new Date(), + updatedAt: new Date(), + logo: null, + defaultLogoIndex: 0, + discoverabilityEnabled: false, + domainBasedMembershipProtectionEnabled: false, + domains: [] + } + return merge(workspace, input) + } + it('throws WorkspaceNotFoundError if the workspace is not found', async () => { + const err = await expectToThrow(async () => { + await updateWorkspaceFactory({ + getWorkspace: async () => null, + emitWorkspaceEvent: async () => { + expect.fail() + }, + upsertWorkspace: async () => { + expect.fail() + } + })({ + workspaceId: cryptoRandomString({ length: 10 }), + workspaceInput: {} + }) + }) + expect(err.message).to.be.equal(new WorkspaceNotFoundError().message) + }) + it('throws from image validator if the workspace logo is invalid', async () => { + const workspace = createTestWorkspaceWithDomainsData() + const err = await expectToThrow(async () => { + await updateWorkspaceFactory({ + getWorkspace: async () => workspace, + emitWorkspaceEvent: async () => { + expect.fail() + }, + upsertWorkspace: async () => { + expect.fail() + } + })({ + workspaceId: workspace.id, + workspaceInput: { + logo: 'a broken logo' + } + }) + }) + expect(err.message).to.be.equal('Provided logo is malformed') + }) + it('validates description length', async () => { + const workspace = createTestWorkspaceWithDomainsData() + const err = await expectToThrow(async () => { + await updateWorkspaceFactory({ + getWorkspace: async () => workspace, + emitWorkspaceEvent: async () => { + expect.fail() + }, + upsertWorkspace: async () => { + expect.fail() + } + })({ + workspaceId: workspace.id, + workspaceInput: { + logo: 'a broken logo' + } + }) + }) + expect(err.message).to.be.equal('Provided logo is malformed') + }) + it('does not allow turning on discoverability if the workspace has no verified domains', async () => { + const workspace = createTestWorkspaceWithDomainsData() + const err = await expectToThrow(async () => { + await updateWorkspaceFactory({ + getWorkspace: async () => workspace, + emitWorkspaceEvent: async () => { + expect.fail() + }, + upsertWorkspace: async () => { + expect.fail() + } + })({ + workspaceId: workspace.id, + workspaceInput: { + discoverabilityEnabled: true + } + }) + }) + expect(err.message).to.be.equal(new WorkspaceNoVerifiedDomainsError().message) + }) + + it('does not allow setting the workspace name to an empty string', async () => { + const workspace = createTestWorkspaceWithDomainsData() + + let newWorkspaceName + await updateWorkspaceFactory({ + getWorkspace: async () => workspace, + emitWorkspaceEvent: async () => { + return [] + }, + upsertWorkspace: async ({ workspace }) => { + newWorkspaceName = workspace.name + } + })({ + workspaceId: workspace.id, + workspaceInput: { name: '' } + }) + expect(newWorkspaceName).to.be.equal(workspace.name) + }) + it('updates the workspace and emits the correct event payload', async () => { + const workspaceId = cryptoRandomString({ length: 10 }) + const workspace = createTestWorkspaceWithDomainsData({ + id: workspaceId, + domains: [ + { + createdAt: new Date(), + createdByUserId: cryptoRandomString({ length: 10 }), + domain: 'example.com', + updatedAt: new Date(), + id: cryptoRandomString({ length: 10 }), + verified: true, + workspaceId + } + ] + }) + + let updatedWorkspace + + const workspaceInput = { + name: cryptoRandomString({ length: 10 }), + discoverabilityEnabled: true + } + + await updateWorkspaceFactory({ + getWorkspace: async () => workspace, + emitWorkspaceEvent: async () => { + return [] + }, + upsertWorkspace: async ({ workspace }) => { + updatedWorkspace = workspace + } + })({ + workspaceId, + workspaceInput + }) + expect(updatedWorkspace!.name).to.be.equal(workspaceInput.name) + expect(updatedWorkspace!.discoverabilityEnabled).to.be.equal( + workspaceInput.discoverabilityEnabled + ) + }) + }) }) type WorkspaceRoleTestContext = {