feat(workspaces): do not allow discoverablity if no verified domains (#2847)

* feat(workspaces): do not allow discoverablity if no verified domains

* test(workspaces): fix tests
This commit is contained in:
Gergő Jedlicska
2024-09-02 15:47:57 +02:00
committed by GitHub
parent f50de80338
commit 0b0853bb3c
5 changed files with 188 additions and 10 deletions
@@ -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 {
@@ -315,7 +315,7 @@ export = FF_WORKSPACES_MODULE_ENABLED
)
const updateWorkspace = updateWorkspaceFactory({
getWorkspace: getWorkspaceFactory({ db }),
getWorkspace: getWorkspaceWithDomainsFactory({ db }),
upsertWorkspace: upsertWorkspaceFactory({ db }),
emitWorkspaceEvent: getEventBus().emit
})
@@ -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<Workspace> => {
// 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()
}
@@ -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
})({
@@ -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> = {}
): 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 = {