Merge branch 'main' into iain/ratelimiter-should-respect-configuration

This commit is contained in:
Iain Sproat
2025-03-04 16:36:43 +00:00
107 changed files with 2650 additions and 1014 deletions
@@ -29,6 +29,7 @@ import { Stream } from '@/modules/core/domain/streams/types'
import { TokenResourceIdentifier } from '@/modules/core/domain/tokens/types'
import { ServerRegion } from '@/modules/multiregion/domain/types'
import { SetOptional } from 'type-fest'
import { WorkspaceSeat, WorkspaceSeatType } from '@/modules/gatekeeper/domain/billing'
/** Workspace */
@@ -385,6 +386,10 @@ export type CopyProjectObjects = (params: {
export type CopyProjectAutomations = (params: {
projectIds: string[]
}) => Promise<Record<string, number>>
export type AssignWorkspaceSeat = (
params: Pick<WorkspaceSeat, 'userId' | 'workspaceId'> & { type?: WorkspaceSeatType }
) => Promise<void>
export type CopyProjectComments = (params: {
projectIds: string[]
}) => Promise<Record<string, number>>
@@ -394,3 +399,10 @@ export type CopyProjectWebhooks = (params: {
export type CopyProjectBlobs = (params: {
projectIds: string[]
}) => Promise<Record<string, number>>
export type SetUserActiveWorkspace = (args: {
userId: string
workspaceSlug: string | null
/** Is the user in a "personal project" outside of a workspace? */
isProjectsActive?: boolean
}) => Promise<void>
@@ -0,0 +1,7 @@
import { BaseError } from '@/modules/shared/errors'
export class InvalidWorkspaceSeatTypeError extends BaseError {
static defaultMessage = 'Workspace seat type is invalid'
static code = 'INDALID_WORKSPACE_SEAT_TYPE_ERROR'
static statusCode = 400
}
@@ -5,6 +5,7 @@ import {
upsertProjectRoleFactory
} from '@/modules/core/repositories/streams'
import {
AssignWorkspaceSeat,
CountWorkspaceRoleWithOptionalProjectRole,
GetDefaultRegion,
GetWorkspace,
@@ -71,7 +72,8 @@ import { getBaseTrackingProperties, getClient } from '@/modules/shared/utils/mix
import {
calculateSubscriptionSeats,
GetWorkspacePlan,
GetWorkspaceSubscription
GetWorkspaceSubscription,
WorkspaceSeatType
} from '@/modules/gatekeeper/domain/billing'
import { getWorkspacePlanProductId } from '@/modules/gatekeeper/stripe'
import { Workspace } from '@/modules/workspacesCore/domain/types'
@@ -81,6 +83,11 @@ import {
getWorkspacePlanFactory,
getWorkspaceSubscriptionFactory
} from '@/modules/gatekeeper/repositories/billing'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
import { assignWorkspaceSeatFactory } from '@/modules/workspaces/services/workspaceSeat'
import { createWorkspaceSeatFactory } from '@/modules/gatekeeper/repositories/workspaceSeat'
const { FF_WORKSPACES_NEW_PLANS_ENABLED } = getFeatureFlags()
export const onProjectCreatedFactory =
({
@@ -222,22 +229,26 @@ export const onWorkspaceRoleUpdatedFactory =
getWorkspaceRoleToDefaultProjectRoleMapping,
queryAllWorkspaceProjects,
deleteProjectRole,
upsertProjectRole
upsertProjectRole,
assignWorkspaceSeat
}: {
getWorkspaceRoleToDefaultProjectRoleMapping: GetWorkspaceRoleToDefaultProjectRoleMapping
queryAllWorkspaceProjects: QueryAllWorkspaceProjects
deleteProjectRole: DeleteProjectRole
upsertProjectRole: UpsertProjectRole
assignWorkspaceSeat: AssignWorkspaceSeat
}) =>
async ({
userId,
role,
workspaceId,
seatType,
flags
}: {
userId: string
role: WorkspaceRoles
workspaceId: string
seatType?: WorkspaceSeatType
flags?: {
skipProjectRoleUpdatesFor: string[]
}
@@ -276,6 +287,10 @@ export const onWorkspaceRoleUpdatedFactory =
})
)
}
if (FF_WORKSPACES_NEW_PLANS_ENABLED) {
await assignWorkspaceSeat({ userId, workspaceId, type: seatType })
}
}
export const workspaceTrackingFactory =
@@ -452,6 +467,21 @@ const emitWorkspaceGraphqlSubscriptionsFactory =
}
}
const onWorkspaceCreatedFactory =
({ assignWorkspaceSeat }: { assignWorkspaceSeat: AssignWorkspaceSeat }) =>
async ({
workspace,
createdByUserId
}: {
workspace: Workspace
createdByUserId: string
}) => {
if (!FF_WORKSPACES_NEW_PLANS_ENABLED) {
return
}
await assignWorkspaceSeat({ userId: createdByUserId, workspaceId: workspace.id })
}
export const initializeEventListenersFactory =
({ db }: { db: Knex }) =>
() => {
@@ -534,10 +564,24 @@ export const initializeEventListenersFactory =
}),
queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({ getStreams }),
deleteProjectRole: deleteProjectRoleFactory({ db: trx }),
upsertProjectRole: upsertProjectRoleFactory({ db: trx })
upsertProjectRole: upsertProjectRoleFactory({ db: trx }),
assignWorkspaceSeat: assignWorkspaceSeatFactory({
createWorkspaceSeat: createWorkspaceSeatFactory({ db: trx }),
getWorkspaceRoleForUser: getWorkspaceRoleForUserFactory({ db: trx })
})
})
await withTransaction(onWorkspaceRoleUpdated(payload), trx)
}),
eventBus.listen(WorkspaceEvents.Created, async ({ payload }) => {
const trx = await db.transaction()
const onWorkspaceCreated = onWorkspaceCreatedFactory({
assignWorkspaceSeat: assignWorkspaceSeatFactory({
createWorkspaceSeat: createWorkspaceSeatFactory({ db: trx }),
getWorkspaceRoleForUser: getWorkspaceRoleForUserFactory({ db: trx })
})
})
await withTransaction(onWorkspaceCreated(payload), trx)
}),
eventBus.listen('**', emitWorkspaceGraphqlSubscriptions)
]
@@ -43,7 +43,6 @@ import { getInvitationTargetUsersFactory } from '@/modules/serverinvites/service
import { authorizeResolver } from '@/modules/shared'
import {
getFeatureFlags,
getServerOrigin,
isRateLimiterEnabled
} from '@/modules/shared/helpers/envHelper'
import { getEventBus } from '@/modules/shared/services/eventBus'
@@ -169,17 +168,12 @@ import {
listWorkspaceSsoMembershipsFactory
} from '@/modules/workspaces/repositories/sso'
import { getDecryptor } from '@/modules/workspaces/helpers/sso'
import { getWorkspaceFunctions } from '@/modules/automate/clients/executionEngine'
import { getFunctionsFactory } from '@/modules/automate/clients/executionEngine'
import {
ExecutionEngineFailedResponseError,
ExecutionEngineNetworkError
} from '@/modules/automate/errors/executionEngine'
import { getDefaultRegionFactory } from '@/modules/workspaces/repositories/regions'
import {
AuthCodePayloadAction,
createStoredAuthCodeFactory
} from '@/modules/automate/services/authCode'
import { getGenericRedis } from '@/modules/shared/redis/redis'
import { convertFunctionToGraphQLReturn } from '@/modules/automate/services/functionManagement'
import {
getWorkspacePlanFactory,
@@ -202,6 +196,13 @@ import { OperationTypeNode } from 'graphql'
import { updateWorkspacePlanFactory } from '@/modules/gatekeeper/services/workspacePlans'
import { GetWorkspaceCollaboratorsArgs } from '@/modules/workspaces/domain/operations'
import { WorkspaceTeamMember } from '@/modules/workspaces/domain/types'
import { UsersMeta } from '@/modules/core/dbSchema'
import { setUserActiveWorkspaceFactory } from '@/modules/workspaces/repositories/users'
import { getGenericRedis } from '@/modules/shared/redis/redis'
import {
AuthCodePayloadAction,
createStoredAuthCodeFactory
} from '@/modules/automate/services/authCode'
const eventBus = getEventBus()
const getServerInfo = getServerInfoFactory({ db })
@@ -1079,6 +1080,13 @@ export = FF_WORKSPACES_MODULE_ENABLED
},
automateFunctions: async (parent, args, context) => {
try {
await authorizeResolver(
context.userId,
parent.id,
Roles.Workspace.Member,
context.resourceAccessRules
)
const authCode = await createStoredAuthCodeFactory({
redis: getGenericRedis()
})({
@@ -1086,14 +1094,18 @@ export = FF_WORKSPACES_MODULE_ENABLED
action: AuthCodePayloadAction.ListWorkspaceFunctions
})
const res = await getWorkspaceFunctions({
workspaceId: parent.id,
query: removeNullOrUndefinedKeys(args),
body: {
speckleServerAuthenticationPayload: {
...authCode,
origin: getServerOrigin()
}
const res = await getFunctionsFactory({
logger: context.log
})({
auth: authCode,
filters: {
query: args.filter?.search ?? undefined,
cursor: args.cursor ?? undefined,
limit: args.limit,
requireRelease: true,
includeFeatured: true,
includeWorkspaces: [parent.id],
includeUsers: []
}
})
@@ -1105,12 +1117,12 @@ export = FF_WORKSPACES_MODULE_ENABLED
}
}
const items = res.functions.map(convertFunctionToGraphQLReturn)
const items = res.items.map(convertFunctionToGraphQLReturn)
return {
cursor: undefined,
totalCount: res.functions.length,
items
items,
cursor: res.cursor,
totalCount: res.totalCount
}
} catch (e) {
const isNotFound =
@@ -1296,6 +1308,26 @@ export = FF_WORKSPACES_MODULE_ENABLED
})
return await getInvites(parent.id)
},
async activeWorkspace(parent, _args, ctx) {
const metaVal = await ctx.loaders.users.getUserMeta.load({
userId: parent.id,
key: UsersMeta.metaKey.activeWorkspace
})
if (!metaVal?.value) return null
return await getWorkspaceBySlugFactory({ db })({
workspaceSlug: metaVal.value
})
},
async isProjectsActive(parent, _args, ctx) {
const metaVal = await ctx.loaders.users.getUserMeta.load({
userId: parent.id,
key: UsersMeta.metaKey.isProjectsActive
})
return !!metaVal?.value
}
},
Project: {
@@ -1386,6 +1418,31 @@ export = FF_WORKSPACES_MODULE_ENABLED
return team
}
},
ActiveUserMutations: {
async setActiveWorkspace(_parent, args, ctx) {
const userId = ctx.userId
if (!userId) return false
await Promise.all([
ctx.loaders.users.getUserMeta.clear({
userId,
key: UsersMeta.metaKey.activeWorkspace
}),
ctx.loaders.users.getUserMeta.clear({
userId,
key: UsersMeta.metaKey.isProjectsActive
})
])
await setUserActiveWorkspaceFactory({ db })({
userId,
workspaceSlug: args.slug ?? null,
isProjectsActive: !!args.isProjectsActive
})
return true
}
},
Subscription: {
workspaceProjectsUpdated: {
subscribe: filteredSubscribe(
@@ -0,0 +1,14 @@
import { Users } from '@/modules/core/dbSchema'
import { metaHelpers } from '@/modules/core/helpers/meta'
import { SetUserActiveWorkspace } from '@/modules/workspaces/domain/operations'
import { Knex } from 'knex'
export const setUserActiveWorkspaceFactory =
(deps: { db: Knex }): SetUserActiveWorkspace =>
async ({ userId, workspaceSlug, isProjectsActive = false }) => {
const meta = metaHelpers(Users, deps.db)
await Promise.all([
meta.set(userId, 'activeWorkspace', workspaceSlug),
meta.set(userId, 'isProjectsActive', isProjectsActive)
])
}
@@ -147,6 +147,10 @@ export const approveWorkspaceJoinRequestFactory =
await upsertWorkspaceRole({ userId, workspaceId, role, createdAt: new Date() })
await emit({ eventName: WorkspaceEvents.Updated, payload: { workspace } })
await emit({
eventName: WorkspaceEvents.RoleUpdated,
payload: { workspaceId, userId, role }
})
await sendWorkspaceJoinRequestApprovedEmail({
workspace,
@@ -0,0 +1,101 @@
import { WorkspaceSeatType } from '@/modules/gatekeeper/domain/billing'
import { CreateWorkspaceSeat } from '@/modules/gatekeeper/domain/operations'
import { NotFoundError } from '@/modules/shared/errors'
import {
AssignWorkspaceSeat,
GetWorkspaceRoleForUser
} from '@/modules/workspaces/domain/operations'
import { InvalidWorkspaceSeatTypeError } from '@/modules/workspaces/errors/workspaceSeat'
import { Roles, WorkspaceRoles } from '@speckle/shared'
import { z } from 'zod'
const getDefaultWorkspaceSeatTypeByWorkspaceRole = ({
workspaceRole
}: {
workspaceRole: WorkspaceRoles
}): WorkspaceSeatType => {
if (workspaceRole === Roles.Workspace.Admin) {
return 'editor'
}
return 'viewer'
}
const WorkspaceRoleWorkspaceSeatTypeMapping = z.union([
z.object({
workspaceRole: z.literal(Roles.Workspace.Admin),
workspaceSeatType: z.literal('editor')
}),
z.object({
workspaceRole: z.literal(Roles.Workspace.Member),
workspaceSeatType: z.union([z.literal('editor'), z.literal('viewer')])
}),
z.object({
workspaceRole: z.literal(Roles.Workspace.Guest),
workspaceSeatType: z.union([z.literal('editor'), z.literal('viewer')])
})
])
type WorkspaceRoleWorkspaceSeatTypeMapping = z.infer<
typeof WorkspaceRoleWorkspaceSeatTypeMapping
>
export const isWorkspaceRoleWorkspaceSeatTypeValid = ({
workspaceRole,
workspaceSeatType
}: {
workspaceRole: WorkspaceRoles
workspaceSeatType: WorkspaceSeatType
}): boolean => {
return WorkspaceRoleWorkspaceSeatTypeMapping.safeParse({
workspaceRole,
workspaceSeatType
}).success
}
export const assignWorkspaceSeatFactory =
({
createWorkspaceSeat,
getWorkspaceRoleForUser
}: {
createWorkspaceSeat: CreateWorkspaceSeat
getWorkspaceRoleForUser: GetWorkspaceRoleForUser
}): AssignWorkspaceSeat =>
async ({ workspaceId, userId, type }) => {
const workspaceAcl = await getWorkspaceRoleForUser({ workspaceId, userId })
if (!workspaceAcl) {
throw new NotFoundError('User does not have a role in the workspace')
}
if (!type) {
return await createWorkspaceSeat({
workspaceId,
userId,
type: type
? type
: getDefaultWorkspaceSeatTypeByWorkspaceRole({
workspaceRole: workspaceAcl.role
})
})
}
if (
!isWorkspaceRoleWorkspaceSeatTypeValid({
workspaceRole: workspaceAcl.role,
workspaceSeatType: type
})
) {
throw new InvalidWorkspaceSeatTypeError(
`User with workspace role ${workspaceAcl.role} cannot have a seat of type ${type}`,
{
info: {
workspaceId,
userId
}
}
)
}
return await createWorkspaceSeat({
workspaceId,
userId,
type
})
}
@@ -246,10 +246,6 @@ export const assignToWorkspace = async (
user: BasicTestUser,
role?: WorkspaceRoles
) => {
if (!FF_WORKSPACES_MODULE_ENABLED) {
return // Just skip
}
const updateWorkspaceRole = updateWorkspaceRoleFactory({
getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }),
findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ db }),
@@ -0,0 +1,99 @@
import { createRandomEmail } from '@/modules/core/helpers/testHelpers'
import {
BasicTestWorkspace,
createTestWorkspace
} from '@/modules/workspaces/tests/helpers/creation'
import { BasicTestUser, createTestUser } from '@/test/authHelper'
import {
SetUserActiveWorkspaceDocument,
UserActiveResourcesDocument
} from '@/test/graphql/generated/graphql'
import { testApolloServer, TestApolloServer } from '@/test/graphqlHelper'
import { beforeEachContext } from '@/test/hooks'
import { BasicTestStream, createTestStream } from '@/test/speckle-helpers/streamHelper'
import { expect } from 'chai'
import cryptoRandomString from 'crypto-random-string'
describe('ActiveUserMutations.setActiveWorkspace', () => {
let apollo: TestApolloServer
const user: BasicTestUser = {
id: '',
name: 'John Legacy Speckle',
email: createRandomEmail()
}
const workspace: BasicTestWorkspace = {
id: '',
ownerId: '',
name: 'My Workspace',
slug: ''
}
const project: BasicTestStream = {
id: '',
ownerId: '',
name: 'My Project',
isPublic: true
}
before(async () => {
await beforeEachContext()
await createTestUser(user)
await createTestWorkspace(workspace, user)
await createTestStream(project, user)
apollo = await testApolloServer({ authUserId: user.id })
})
it('should accurately report active workspace', async () => {
const resA = await apollo.execute(SetUserActiveWorkspaceDocument, {
slug: workspace.slug
})
expect(resA).to.not.haveGraphQLErrors()
const resB = await apollo.execute(UserActiveResourcesDocument, {})
expect(resB).to.not.haveGraphQLErrors()
expect(resB?.data?.activeUser?.activeWorkspace?.id).to.equal(workspace.id)
})
it('should accurately report if last visited project is not a workspace project', async () => {
const resA = await apollo.execute(SetUserActiveWorkspaceDocument, {
slug: null,
isProjectsActive: true
})
expect(resA).to.not.haveGraphQLErrors()
const resB = await apollo.execute(UserActiveResourcesDocument, {})
expect(resB).to.not.haveGraphQLErrors()
expect(resB?.data?.activeUser?.isProjectsActive).to.be.true
})
it('should allow values to be cleared with null input', async () => {
const resA = await apollo.execute(SetUserActiveWorkspaceDocument, {
slug: workspace.slug
})
expect(resA).to.not.haveGraphQLErrors()
const resB = await apollo.execute(SetUserActiveWorkspaceDocument, { slug: null })
expect(resB).to.not.haveGraphQLErrors()
const resC = await apollo.execute(UserActiveResourcesDocument, {})
expect(resC).to.not.haveGraphQLErrors()
expect(resC.data?.activeUser?.activeWorkspace).to.be.null
})
it('should return null if workspace is not found or was deleted', async () => {
const resA = await apollo.execute(SetUserActiveWorkspaceDocument, {
slug: cryptoRandomString({ length: 9 })
})
expect(resA).to.not.haveGraphQLErrors()
const resB = await apollo.execute(UserActiveResourcesDocument, {})
expect(resB).to.not.haveGraphQLErrors()
expect(resB?.data?.activeUser?.activeWorkspace).to.be.null
})
})
@@ -0,0 +1,227 @@
import { db } from '@/db/knex'
import {
createRandomEmail,
createRandomString
} from '@/modules/core/helpers/testHelpers'
import { createWorkspaceSeatFactory } from '@/modules/gatekeeper/repositories/workspaceSeat'
import { NotFoundError } from '@/modules/shared/errors'
import { InvalidWorkspaceSeatTypeError } from '@/modules/workspaces/errors/workspaceSeat'
import { getWorkspaceRoleForUserFactory } from '@/modules/workspaces/repositories/workspaces'
import { assignWorkspaceSeatFactory } from '@/modules/workspaces/services/workspaceSeat'
import {
assignToWorkspace,
BasicTestWorkspace,
createTestWorkspace
} from '@/modules/workspaces/tests/helpers/creation'
import { expectToThrow } from '@/test/assertionHelper'
import { BasicTestUser, createTestUser } from '@/test/authHelper'
import { Roles } from '@speckle/shared'
import { expect } from 'chai'
import cryptoRandomString from 'crypto-random-string'
describe('Workspace workspaceSeat services', () => {
describe('assignWorkspaceSeatFactory', () => {
it('should throw an error if user is not a member of the workspace', async () => {
const workspaceAdmin: BasicTestUser = {
id: createRandomString(),
name: createRandomString(),
email: createRandomEmail(),
role: Roles.Server.Admin,
verified: true
}
await createTestUser(workspaceAdmin)
const workspace: BasicTestWorkspace = {
id: createRandomString(),
slug: createRandomString(),
ownerId: workspaceAdmin.id,
name: cryptoRandomString({ length: 6 }),
description: cryptoRandomString({ length: 12 })
}
await createTestWorkspace(workspace, workspaceAdmin)
const user: BasicTestUser = {
id: createRandomString(),
name: createRandomString(),
email: createRandomEmail(),
role: Roles.Server.User,
verified: true
}
await createTestUser(user)
const err = await expectToThrow(() =>
assignWorkspaceSeatFactory({
createWorkspaceSeat: createWorkspaceSeatFactory({ db }),
getWorkspaceRoleForUser: getWorkspaceRoleForUserFactory({ db })
})({ userId: user.id, workspaceId: workspace.id, type: 'editor' })
)
expect(err.name).to.eq(NotFoundError.name)
})
it('should assign a workspace seat with the default type if none is provided', async () => {
const workspaceAdmin: BasicTestUser = {
id: createRandomString(),
name: createRandomString(),
email: createRandomEmail(),
role: Roles.Server.Admin,
verified: true
}
await createTestUser(workspaceAdmin)
const workspace: BasicTestWorkspace = {
id: createRandomString(),
slug: createRandomString(),
ownerId: workspaceAdmin.id,
name: cryptoRandomString({ length: 6 }),
description: cryptoRandomString({ length: 12 })
}
await createTestWorkspace(workspace, workspaceAdmin)
const user: BasicTestUser = {
id: createRandomString(),
name: createRandomString(),
email: createRandomEmail(),
role: Roles.Server.User,
verified: true
}
await createTestUser(user)
await assignToWorkspace(workspace, user, Roles.Workspace.Member)
await assignWorkspaceSeatFactory({
createWorkspaceSeat: createWorkspaceSeatFactory({ db }),
getWorkspaceRoleForUser: getWorkspaceRoleForUserFactory({ db })
})({ userId: user.id, workspaceId: workspace.id })
const workspaceSeat = await db('workspace_seats')
.where({ userId: user.id, workspaceId: workspace.id })
.first()
expect(workspaceSeat.type).to.eq('viewer')
})
it('should assign a workspace seat with the provided type', async () => {
const workspaceAdmin: BasicTestUser = {
id: createRandomString(),
name: createRandomString(),
email: createRandomEmail(),
role: Roles.Server.Admin,
verified: true
}
await createTestUser(workspaceAdmin)
const workspace: BasicTestWorkspace = {
id: createRandomString(),
slug: createRandomString(),
ownerId: workspaceAdmin.id,
name: cryptoRandomString({ length: 6 }),
description: cryptoRandomString({ length: 12 })
}
await createTestWorkspace(workspace, workspaceAdmin)
const user: BasicTestUser = {
id: createRandomString(),
name: createRandomString(),
email: createRandomEmail(),
role: Roles.Server.User,
verified: true
}
await createTestUser(user)
await assignToWorkspace(workspace, user, Roles.Workspace.Member)
await assignWorkspaceSeatFactory({
createWorkspaceSeat: createWorkspaceSeatFactory({ db }),
getWorkspaceRoleForUser: getWorkspaceRoleForUserFactory({ db })
})({ userId: user.id, workspaceId: workspace.id, type: 'editor' })
const workspaceSeat = await db('workspace_seats')
.where({ userId: user.id, workspaceId: workspace.id })
.first()
expect(workspaceSeat.type).to.eq('editor')
})
it('should throw an error if seat type is not compatible with workspace role', async () => {
const workspaceAdmin: BasicTestUser = {
id: createRandomString(),
name: createRandomString(),
email: createRandomEmail(),
role: Roles.Server.Admin,
verified: true
}
await createTestUser(workspaceAdmin)
const workspace: BasicTestWorkspace = {
id: createRandomString(),
slug: createRandomString(),
ownerId: workspaceAdmin.id,
name: cryptoRandomString({ length: 6 }),
description: cryptoRandomString({ length: 12 })
}
await createTestWorkspace(workspace, workspaceAdmin)
const user: BasicTestUser = {
id: createRandomString(),
name: createRandomString(),
email: createRandomEmail(),
role: Roles.Server.User,
verified: true
}
await createTestUser(user)
await assignToWorkspace(workspace, user, Roles.Workspace.Admin)
const err = await expectToThrow(() =>
assignWorkspaceSeatFactory({
createWorkspaceSeat: createWorkspaceSeatFactory({ db }),
getWorkspaceRoleForUser: getWorkspaceRoleForUserFactory({ db })
})({ userId: user.id, workspaceId: workspace.id, type: 'viewer' })
)
expect(err.name).to.eq(InvalidWorkspaceSeatTypeError.name)
})
it('should update seat type on role change', async () => {
const workspaceAdmin: BasicTestUser = {
id: createRandomString(),
name: createRandomString(),
email: createRandomEmail(),
role: Roles.Server.Admin,
verified: true
}
await createTestUser(workspaceAdmin)
const workspace: BasicTestWorkspace = {
id: createRandomString(),
slug: createRandomString(),
ownerId: workspaceAdmin.id,
name: cryptoRandomString({ length: 6 }),
description: cryptoRandomString({ length: 12 })
}
await createTestWorkspace(workspace, workspaceAdmin)
const user: BasicTestUser = {
id: createRandomString(),
name: createRandomString(),
email: createRandomEmail(),
role: Roles.Server.User,
verified: true
}
await createTestUser(user)
await assignToWorkspace(workspace, user, Roles.Workspace.Member)
const workspaceSeat = await db('workspace_seats')
.where({ userId: user.id, workspaceId: workspace.id })
.first()
expect(workspaceSeat.type).to.eq('viewer')
// Change workspace role
await assignToWorkspace(workspace, user, Roles.Workspace.Admin)
const workspaceSeatUpdated = await db('workspace_seats')
.where({ userId: user.id, workspaceId: workspace.id })
.first()
expect(workspaceSeatUpdated.type).to.eq('editor')
})
})
})
@@ -84,7 +84,8 @@ describe('Event handlers', () => {
},
upsertProjectRole: async () => {
expect.fail()
}
},
assignWorkspaceSeat: async () => undefined
})({
role: Roles.Workspace.Guest,
userId: cryptoRandomString({ length: 10 }),
@@ -123,7 +124,8 @@ describe('Event handlers', () => {
storedRoles.push(args)
trackProjectUpdate = trackProjectUpdate || options?.trackProjectUpdate
return {} as StreamRecord
}
},
assignWorkspaceSeat: async () => undefined
})({
role: Roles.Workspace.Member,
userId,
@@ -0,0 +1,50 @@
import { isWorkspaceRoleWorkspaceSeatTypeValid } from '@/modules/workspaces/services/workspaceSeat'
import { Roles } from '@speckle/shared'
import { expect } from 'chai'
describe('Workspace workspaceSeat services', () => {
describe('isWorkspaceRoleWorkspaceSeatTypeValid', () => {
it('should return true if the role is admin and seat type is editor', () => {
const result = isWorkspaceRoleWorkspaceSeatTypeValid({
workspaceRole: Roles.Workspace.Admin,
workspaceSeatType: 'editor'
})
expect(result).to.be.true
})
it('should return false if the role is admin and seat type is viewer', () => {
const result = isWorkspaceRoleWorkspaceSeatTypeValid({
workspaceRole: Roles.Workspace.Admin,
workspaceSeatType: 'viewer' as 'editor'
})
expect(result).to.be.false
})
it('should return true if the role is member and seat type is editor', () => {
const result = isWorkspaceRoleWorkspaceSeatTypeValid({
workspaceRole: Roles.Workspace.Member,
workspaceSeatType: 'editor'
})
expect(result).to.be.true
})
it('should return true if the role is member and seat type is viewer', () => {
const result = isWorkspaceRoleWorkspaceSeatTypeValid({
workspaceRole: Roles.Workspace.Member,
workspaceSeatType: 'viewer'
})
expect(result).to.be.true
})
it('should return true if the role is guest and seat type is editor', () => {
const result = isWorkspaceRoleWorkspaceSeatTypeValid({
workspaceRole: Roles.Workspace.Guest,
workspaceSeatType: 'editor'
})
expect(result).to.be.true
})
it('should return true if the role is member and seat type is viewer', () => {
const result = isWorkspaceRoleWorkspaceSeatTypeValid({
workspaceRole: Roles.Workspace.Guest,
workspaceSeatType: 'viewer'
})
expect(result).to.be.true
})
})
})