Merge branch 'main' into iain/ratelimiter-should-respect-configuration
This commit is contained in:
@@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user