feat(gatekeeper): readOnly validation logic
This commit is contained in:
@@ -4152,6 +4152,8 @@ export type Workspace = {
|
||||
name: Scalars['String']['output'];
|
||||
plan?: Maybe<WorkspacePlan>;
|
||||
projects: ProjectCollection;
|
||||
/** A Workspace is marked as readOnly if its trial period is finished or a paid plan is subscribed but payment has failed */
|
||||
readOnly: Scalars['Boolean']['output'];
|
||||
/** Active user's role for this workspace. `null` if request is not authenticated, or the workspace is not explicitly shared with you. */
|
||||
role?: Maybe<Scalars['String']['output']>;
|
||||
slug: Scalars['String']['output'];
|
||||
@@ -7994,6 +7996,7 @@ export type WorkspaceFieldArgs = {
|
||||
name: {},
|
||||
plan: {},
|
||||
projects: WorkspaceProjectsArgs,
|
||||
readOnly: {},
|
||||
role: {},
|
||||
slug: {},
|
||||
sso: {},
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '@/modules/core/repositories/objects'
|
||||
import { createObjectsFactory } from '@/modules/core/services/objects/management'
|
||||
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
||||
import coreModule from '@/modules/core'
|
||||
|
||||
type GetObjectChildrenQueryParams = Parameters<
|
||||
ReturnType<typeof getObjectChildrenQueryFactory>
|
||||
@@ -100,6 +101,10 @@ export = {
|
||||
context.resourceAccessRules
|
||||
)
|
||||
|
||||
await coreModule.executeHooks?.('onCreateObjectRequest', {
|
||||
projectId: args.objectInput.streamId
|
||||
})
|
||||
|
||||
const projectDB = await getProjectDbClient({
|
||||
projectId: args.objectInput.streamId
|
||||
})
|
||||
|
||||
@@ -20,11 +20,12 @@ import {
|
||||
import { validatePermissionsWriteStreamFactory } from '@/modules/core/services/streams/auth'
|
||||
import { authorizeResolver, validateScopes } from '@/modules/shared'
|
||||
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
||||
import { ExecuteHooks } from '@/modules/core/hooks'
|
||||
|
||||
const MAX_FILE_SIZE = maximumObjectUploadFileSizeMb() * 1024 * 1024
|
||||
const { FF_NO_CLOSURE_WRITES } = getFeatureFlags()
|
||||
|
||||
export default (app: Router) => {
|
||||
export default (app: Router, { executeHooks }: { executeHooks: ExecuteHooks }) => {
|
||||
const validatePermissionsWriteStream = validatePermissionsWriteStreamFactory({
|
||||
validateScopes,
|
||||
authorizeResolver
|
||||
@@ -63,6 +64,10 @@ export default (app: Router) => {
|
||||
return res.status(hasStreamAccess.status).end()
|
||||
}
|
||||
|
||||
await executeHooks('onCreateObjectRequest', {
|
||||
projectId: req.params.streamId
|
||||
})
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId: req.params.streamId })
|
||||
|
||||
const objectInsertionService = FF_NO_CLOSURE_WRITES
|
||||
|
||||
@@ -363,7 +363,10 @@ describe('GraphQL API Core @core-api', () => {
|
||||
const res = await sendRequest(userA.token, {
|
||||
query: 'mutation($user:UserUpdateInput!) { userUpdate( user: $user) } ',
|
||||
variables: {
|
||||
user: { name: 'Miticå', bio: 'He never really knows what he is doing.' }
|
||||
user: {
|
||||
name: 'test user updated',
|
||||
bio: 'He never really knows what he is doing.'
|
||||
}
|
||||
}
|
||||
})
|
||||
expect(res).to.be.json
|
||||
@@ -1215,7 +1218,7 @@ describe('GraphQL API Core @core-api', () => {
|
||||
expect(res).to.be.json
|
||||
expect(res.body.errors).to.not.exist
|
||||
expect(res.body.data).to.have.property('user')
|
||||
expect(res.body.data.user.name).to.equal('Miticå')
|
||||
expect(res.body.data.user.name).to.equal('test user updated')
|
||||
expect(res.body.data.user.email).to.equal('d.1@speckle.systems')
|
||||
expect(res.body.data.user.role).to.equal(Roles.Server.Admin)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { beforeEachContext } from '@/test/hooks'
|
||||
import { expect } from 'chai'
|
||||
import { describe, it } from 'mocha'
|
||||
import {
|
||||
createRandomEmail,
|
||||
createRandomPassword
|
||||
} from '@/modules/core/helpers/testHelpers'
|
||||
import {
|
||||
createUserEmailFactory,
|
||||
ensureNoPrimaryEmailForUserFactory,
|
||||
findEmailFactory
|
||||
} from '@/modules/core/repositories/userEmails'
|
||||
import { db } from '@/db/knex'
|
||||
import { testApolloServer } from '@/test/graphqlHelper'
|
||||
import {
|
||||
CreateObjectDocument,
|
||||
CreateWorkspaceDocument,
|
||||
CreateWorkspaceProjectDocument
|
||||
} from '@/test/graphql/generated/graphql'
|
||||
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
|
||||
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
|
||||
import {
|
||||
deleteServerOnlyInvitesFactory,
|
||||
updateAllInviteTargetsFactory
|
||||
} from '@/modules/serverinvites/repositories/serverInvites'
|
||||
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
|
||||
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
|
||||
import { renderEmail } from '@/modules/emails/services/emailRendering'
|
||||
import { sendEmail } from '@/modules/emails/services/sending'
|
||||
import {
|
||||
countAdminUsersFactory,
|
||||
legacyGetUserFactory,
|
||||
storeUserAclFactory,
|
||||
storeUserFactory
|
||||
} from '@/modules/core/repositories/users'
|
||||
import { createUserFactory } from '@/modules/core/services/users/management'
|
||||
import { UsersEmitter } from '@/modules/core/events/usersEmitter'
|
||||
import { getServerInfoFactory } from '@/modules/core/repositories/server'
|
||||
import { WorkspaceReadOnlyError } from '@/modules/gatekeeper/errors/billing'
|
||||
|
||||
const getServerInfo = getServerInfoFactory({ db })
|
||||
const getUser = legacyGetUserFactory({ db })
|
||||
const requestNewEmailVerification = requestNewEmailVerificationFactory({
|
||||
findEmail: findEmailFactory({ db }),
|
||||
getUser,
|
||||
getServerInfo,
|
||||
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
|
||||
renderEmail,
|
||||
sendEmail
|
||||
})
|
||||
|
||||
const createUserEmail = validateAndCreateUserEmailFactory({
|
||||
createUserEmail: createUserEmailFactory({ db }),
|
||||
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
|
||||
findEmail: findEmailFactory({ db }),
|
||||
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
|
||||
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
|
||||
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
|
||||
}),
|
||||
requestNewEmailVerification
|
||||
})
|
||||
|
||||
const findEmail = findEmailFactory({ db })
|
||||
const createUser = createUserFactory({
|
||||
getServerInfo,
|
||||
findEmail,
|
||||
storeUser: storeUserFactory({ db }),
|
||||
countAdminUsers: countAdminUsersFactory({ db }),
|
||||
storeUserAcl: storeUserAclFactory({ db }),
|
||||
validateAndCreateUserEmail: createUserEmail,
|
||||
usersEventsEmitter: UsersEmitter.emit
|
||||
})
|
||||
|
||||
describe('Objects graphql @core', () => {
|
||||
before(async () => {
|
||||
await beforeEachContext()
|
||||
})
|
||||
|
||||
describe('objectCreate mutation', () => {
|
||||
it('should return error if project is read-only', async () => {
|
||||
const userId = await createUser({
|
||||
name: 'emails user',
|
||||
email: createRandomEmail(),
|
||||
password: createRandomPassword()
|
||||
})
|
||||
|
||||
const apollo = await testApolloServer({ authUserId: userId })
|
||||
|
||||
const workspaceCreateRes = await apollo.execute(CreateWorkspaceDocument, {
|
||||
input: { name: 'test ws' }
|
||||
})
|
||||
expect(workspaceCreateRes).to.not.haveGraphQLErrors()
|
||||
|
||||
const workspace = workspaceCreateRes.data?.workspaceMutations.create
|
||||
|
||||
const projectCreateRes = await apollo.execute(CreateWorkspaceProjectDocument, {
|
||||
input: { workspaceId: workspace!.id, name: 'test project' }
|
||||
})
|
||||
expect(projectCreateRes).to.not.haveGraphQLErrors()
|
||||
const project = projectCreateRes.data?.workspaceMutations.projects.create
|
||||
|
||||
// Make the project read-only
|
||||
await db('workspace_plans')
|
||||
.update({ status: 'expired' })
|
||||
.where({ workspaceId: workspace!.id })
|
||||
|
||||
const objectCreateRes = await apollo.execute(CreateObjectDocument, {
|
||||
input: {
|
||||
streamId: project!.id,
|
||||
objects: [
|
||||
{
|
||||
id: 'e5262a6fb51540974e6d07ac60b7fe5c',
|
||||
name: 'Rhino Model',
|
||||
elements: [
|
||||
{
|
||||
referencedId: '581a822cdaa5c2972783510d57617f73',
|
||||
/* eslint-disable camelcase */
|
||||
speckle_type: 'reference'
|
||||
}
|
||||
],
|
||||
__closure: {
|
||||
'0086c072ee1fd70ac0a68c067a37e0eb': 3
|
||||
},
|
||||
speckleType: 'Speckle.Core.Models.Collection',
|
||||
speckle_type: 'Speckle.Core.Models.Collection',
|
||||
applicationId: null,
|
||||
collectionType: 'rhino model',
|
||||
totalChildrenCount: 610
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
expect(objectCreateRes).to.haveGraphQLErrors()
|
||||
expect(objectCreateRes.errors).to.have.length(1)
|
||||
expect(objectCreateRes.errors![0].message).to.eq(
|
||||
new WorkspaceReadOnlyError().message
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,165 @@
|
||||
import { db } from '@/db/knex'
|
||||
import { UsersEmitter } from '@/modules/core/events/usersEmitter'
|
||||
import {
|
||||
createRandomEmail,
|
||||
createRandomPassword
|
||||
} from '@/modules/core/helpers/testHelpers'
|
||||
import { getServerInfoFactory } from '@/modules/core/repositories/server'
|
||||
import {
|
||||
createUserEmailFactory,
|
||||
ensureNoPrimaryEmailForUserFactory,
|
||||
findEmailFactory
|
||||
} from '@/modules/core/repositories/userEmails'
|
||||
import {
|
||||
countAdminUsersFactory,
|
||||
legacyGetUserFactory,
|
||||
storeUserAclFactory,
|
||||
storeUserFactory
|
||||
} from '@/modules/core/repositories/users'
|
||||
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
|
||||
import { createUserFactory } from '@/modules/core/services/users/management'
|
||||
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
|
||||
import { renderEmail } from '@/modules/emails/services/emailRendering'
|
||||
import { sendEmail } from '@/modules/emails/services/sending'
|
||||
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
|
||||
import {
|
||||
deleteServerOnlyInvitesFactory,
|
||||
updateAllInviteTargetsFactory
|
||||
} from '@/modules/serverinvites/repositories/serverInvites'
|
||||
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
|
||||
import { createTestWorkspace } from '@/modules/workspaces/tests/helpers/creation'
|
||||
import { beforeEachContext } from '@/test/hooks'
|
||||
import { BasicTestStream, createTestStream } from '@/test/speckle-helpers/streamHelper'
|
||||
import request from 'supertest'
|
||||
import { Express } from 'express'
|
||||
import { createPersonalAccessTokenFactory } from '@/modules/core/services/tokens'
|
||||
import {
|
||||
storeApiTokenFactory,
|
||||
storePersonalApiTokenFactory,
|
||||
storeTokenResourceAccessDefinitionsFactory,
|
||||
storeTokenScopesFactory
|
||||
} from '@/modules/core/repositories/tokens'
|
||||
import { Scopes } from '@speckle/shared'
|
||||
import { expect } from 'chai'
|
||||
|
||||
const getServerInfo = getServerInfoFactory({ db })
|
||||
const getUser = legacyGetUserFactory({ db })
|
||||
const requestNewEmailVerification = requestNewEmailVerificationFactory({
|
||||
findEmail: findEmailFactory({ db }),
|
||||
getUser,
|
||||
getServerInfo,
|
||||
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
|
||||
renderEmail,
|
||||
sendEmail
|
||||
})
|
||||
|
||||
const createUserEmail = validateAndCreateUserEmailFactory({
|
||||
createUserEmail: createUserEmailFactory({ db }),
|
||||
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
|
||||
findEmail: findEmailFactory({ db }),
|
||||
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
|
||||
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
|
||||
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
|
||||
}),
|
||||
requestNewEmailVerification
|
||||
})
|
||||
|
||||
const findEmail = findEmailFactory({ db })
|
||||
const createUser = createUserFactory({
|
||||
getServerInfo,
|
||||
findEmail,
|
||||
storeUser: storeUserFactory({ db }),
|
||||
countAdminUsers: countAdminUsersFactory({ db }),
|
||||
storeUserAcl: storeUserAclFactory({ db }),
|
||||
validateAndCreateUserEmail: createUserEmail,
|
||||
usersEventsEmitter: UsersEmitter.emit
|
||||
})
|
||||
|
||||
const createPersonalAccessToken = createPersonalAccessTokenFactory({
|
||||
storeApiToken: storeApiTokenFactory({ db }),
|
||||
storeTokenScopes: storeTokenScopesFactory({ db }),
|
||||
storeTokenResourceAccessDefinitions: storeTokenResourceAccessDefinitionsFactory({
|
||||
db
|
||||
}),
|
||||
storePersonalApiToken: storePersonalApiTokenFactory({ db })
|
||||
})
|
||||
|
||||
describe('Objects REST @core', () => {
|
||||
let app: Express
|
||||
before(async () => {
|
||||
;({ app } = await beforeEachContext())
|
||||
})
|
||||
|
||||
it('should return an error if the project is read-only', async () => {
|
||||
const userId = await createUser({
|
||||
name: 'emails user',
|
||||
email: createRandomEmail(),
|
||||
password: createRandomPassword()
|
||||
})
|
||||
const user = await getUser(userId)
|
||||
const workspace = {
|
||||
name: 'Test Workspace #1',
|
||||
ownerId: userId,
|
||||
id: '',
|
||||
slug: ''
|
||||
}
|
||||
await createTestWorkspace(workspace, user, {
|
||||
addPlan: { name: 'business', status: 'expired' }
|
||||
})
|
||||
|
||||
const project = {
|
||||
id: '',
|
||||
name: 'test project',
|
||||
ownerId: userId,
|
||||
workspaceId: workspace.id
|
||||
}
|
||||
await createTestStream(project as unknown as BasicTestStream, user)
|
||||
|
||||
const token = `Bearer ${await createPersonalAccessToken(
|
||||
user.id,
|
||||
'test token user A',
|
||||
[
|
||||
Scopes.Streams.Read,
|
||||
Scopes.Streams.Write,
|
||||
Scopes.Users.Read,
|
||||
Scopes.Users.Email,
|
||||
Scopes.Tokens.Write,
|
||||
Scopes.Tokens.Read,
|
||||
Scopes.Profile.Read,
|
||||
Scopes.Profile.Email
|
||||
]
|
||||
)}`
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/objects/${project.id}`)
|
||||
.set('Authorization', token)
|
||||
.set('Content-type', 'multipart/form-data')
|
||||
.attach(
|
||||
'batch1',
|
||||
Buffer.from(
|
||||
JSON.stringify({
|
||||
id: 'e5262a6fb51540974e6d07ac60b7fe5c',
|
||||
name: 'Rhino Model',
|
||||
elements: [
|
||||
{
|
||||
referencedId: '581a822cdaa5c2972783510d57617f73',
|
||||
/* eslint-disable camelcase */
|
||||
speckle_type: 'reference'
|
||||
}
|
||||
],
|
||||
__closure: {
|
||||
'0086c072ee1fd70ac0a68c067a37e0eb': 3
|
||||
},
|
||||
speckleType: 'Speckle.Core.Models.Collection',
|
||||
speckle_type: 'Speckle.Core.Models.Collection',
|
||||
applicationId: null,
|
||||
collectionType: 'rhino model',
|
||||
totalChildrenCount: 610
|
||||
}),
|
||||
'utf8'
|
||||
)
|
||||
)
|
||||
|
||||
expect(res).to.have.status(403)
|
||||
})
|
||||
})
|
||||
@@ -13,3 +13,9 @@ export type WorkspaceFeatureAccessFunction = (args: {
|
||||
export type ChangeExpiredTrialWorkspacePlanStatuses = (args: {
|
||||
numberOfDays: number
|
||||
}) => Promise<WorkspacePlan[]>
|
||||
|
||||
export type GetWorkspacePlanByProjectId = ({
|
||||
projectId
|
||||
}: {
|
||||
projectId: string
|
||||
}) => Promise<WorkspacePlan | null>
|
||||
|
||||
@@ -53,3 +53,9 @@ export class WorkspacePlanDowngradeError extends BaseError {
|
||||
static code = 'WORKSPACE_PLAN_DOWNGRADE_ERROR'
|
||||
static statusCode = 400
|
||||
}
|
||||
|
||||
export class WorkspaceReadOnlyError extends BaseError {
|
||||
static defaultMessage = 'Workspace is read-only'
|
||||
static code = 'WORKSPACE_READ_ONLY_ERROR'
|
||||
static statusCode = 403
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from '@/modules/gatekeeper/services/subscriptions'
|
||||
import {
|
||||
changeExpiredTrialWorkspacePlanStatusesFactory,
|
||||
getWorkspacePlanByProjectIdFactory,
|
||||
getWorkspacePlanFactory,
|
||||
getWorkspaceSubscriptionsPastBillingCycleEndFactory,
|
||||
upsertWorkspaceSubscriptionFactory
|
||||
@@ -28,6 +29,9 @@ import { countWorkspaceRoleWithOptionalProjectRoleFactory } from '@/modules/work
|
||||
import { reconcileWorkspaceSubscriptionFactory } from '@/modules/gatekeeper/clients/stripe'
|
||||
import { ScheduleExecution } from '@/modules/core/domain/scheduledTasks/operations'
|
||||
import { EventBusEmit, getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import coreModule from '@/modules/core/index'
|
||||
import { isProjectReadOnlyFactory } from '@/modules/gatekeeper/services/readOnly'
|
||||
import { WorkspaceReadOnlyError } from '@/modules/gatekeeper/errors/billing'
|
||||
|
||||
const { FF_GATEKEEPER_MODULE_ENABLED, FF_BILLING_INTEGRATION_ENABLED } =
|
||||
getFeatureFlags()
|
||||
@@ -166,6 +170,19 @@ const gatekeeperModule: SpeckleModule = {
|
||||
scheduledTasks.forEach((task) => {
|
||||
task.stop()
|
||||
})
|
||||
},
|
||||
async finalize() {
|
||||
coreModule.addHook(
|
||||
'onCreateObjectRequest',
|
||||
async function isProjectReadOnly({ projectId }) {
|
||||
const readOnly = await isProjectReadOnlyFactory({
|
||||
getWorkspacePlanByProjectId: getWorkspacePlanByProjectIdFactory({
|
||||
db
|
||||
})
|
||||
})({ projectId })
|
||||
if (readOnly) throw new WorkspaceReadOnlyError()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
export = gatekeeperModule
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Streams } from '@/modules/core/dbSchema'
|
||||
import {
|
||||
CheckoutSession,
|
||||
GetCheckoutSession,
|
||||
@@ -17,7 +18,11 @@ import {
|
||||
UpsertTrialWorkspacePlan,
|
||||
UpsertUnpaidWorkspacePlan
|
||||
} from '@/modules/gatekeeper/domain/billing'
|
||||
import { ChangeExpiredTrialWorkspacePlanStatuses } from '@/modules/gatekeeper/domain/operations'
|
||||
import {
|
||||
ChangeExpiredTrialWorkspacePlanStatuses,
|
||||
GetWorkspacePlanByProjectId
|
||||
} from '@/modules/gatekeeper/domain/operations'
|
||||
import { Workspaces } from '@/modules/workspacesCore/helpers/db'
|
||||
import { Knex } from 'knex'
|
||||
|
||||
const tables = {
|
||||
@@ -165,3 +170,20 @@ export const getWorkspaceSubscriptionsPastBillingCycleEndFactory =
|
||||
.select()
|
||||
.where('currentBillingCycleEnd', '<', cycleEnd)
|
||||
}
|
||||
|
||||
export const getWorkspacePlanByProjectIdFactory =
|
||||
({ db }: { db: Knex }): GetWorkspacePlanByProjectId =>
|
||||
async ({ projectId }) => {
|
||||
return await tables
|
||||
.workspacePlans(db)
|
||||
.select([
|
||||
'workspace_plans.workspaceId',
|
||||
'workspace_plans.status',
|
||||
'workspace_plans.name',
|
||||
'workspace_plans.createdAt'
|
||||
])
|
||||
.innerJoin(Workspaces.name, Workspaces.col.id, 'workspace_plans.workspaceId')
|
||||
.innerJoin(Streams.name, Streams.col.workspaceId, Workspaces.col.id)
|
||||
.where({ [Streams.col.id]: projectId })
|
||||
.first<WorkspacePlan | null>()
|
||||
}
|
||||
|
||||
@@ -1,24 +1,40 @@
|
||||
import { GetWorkspacePlan } from '@/modules/gatekeeper/domain/billing'
|
||||
import { GetWorkspacePlan, WorkspacePlan } from '@/modules/gatekeeper/domain/billing'
|
||||
import { GetWorkspacePlanByProjectId } from '@/modules/gatekeeper/domain/operations'
|
||||
import { Workspace } from '@/modules/workspacesCore/domain/types'
|
||||
import { throwUncoveredError } from '@speckle/shared'
|
||||
|
||||
const isWorkspacePlanStatusReadOnly = (status: WorkspacePlan['status']) => {
|
||||
switch (status) {
|
||||
case 'cancelationScheduled':
|
||||
case 'valid':
|
||||
case 'trial':
|
||||
case 'paymentFailed':
|
||||
return false
|
||||
case 'expired':
|
||||
case 'canceled':
|
||||
return true
|
||||
default:
|
||||
throwUncoveredError(status)
|
||||
}
|
||||
}
|
||||
|
||||
export const isWorkspaceReadOnlyFactory =
|
||||
({ getWorkspacePlan }: { getWorkspacePlan: GetWorkspacePlan }) =>
|
||||
async ({ workspaceId }: { workspaceId: Workspace['id'] }) => {
|
||||
const workspacePlan = await getWorkspacePlan({ workspaceId })
|
||||
// Should never happen
|
||||
if (!workspacePlan) return true
|
||||
|
||||
switch (workspacePlan.status) {
|
||||
case 'cancelationScheduled':
|
||||
case 'valid':
|
||||
case 'trial':
|
||||
case 'paymentFailed':
|
||||
return false
|
||||
case 'expired':
|
||||
case 'canceled':
|
||||
return true
|
||||
default:
|
||||
throwUncoveredError(workspacePlan)
|
||||
}
|
||||
return isWorkspacePlanStatusReadOnly(workspacePlan.status)
|
||||
}
|
||||
|
||||
export const isProjectReadOnlyFactory =
|
||||
({
|
||||
getWorkspacePlanByProjectId
|
||||
}: {
|
||||
getWorkspacePlanByProjectId: GetWorkspacePlanByProjectId
|
||||
}) =>
|
||||
async ({ projectId }: { projectId: string }) => {
|
||||
const workspacePlan = await getWorkspacePlanByProjectId({ projectId })
|
||||
if (!workspacePlan) return false // The project is not in a workspace
|
||||
return isWorkspacePlanStatusReadOnly(workspacePlan.status)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { GetWorkspacePlan } from '@/modules/gatekeeper/domain/billing'
|
||||
import { isWorkspaceReadOnlyFactory } from '@/modules/gatekeeper/services/readOnly'
|
||||
import { GetWorkspacePlanByProjectId } from '@/modules/gatekeeper/domain/operations'
|
||||
import {
|
||||
isProjectReadOnlyFactory,
|
||||
isWorkspaceReadOnlyFactory
|
||||
} from '@/modules/gatekeeper/services/readOnly'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('@gatekeeper readOnly', () => {
|
||||
@@ -53,4 +57,88 @@ describe('@gatekeeper readOnly', () => {
|
||||
expect(await isWorkspaceReadOnly({ workspaceId: '' })).to.be.false
|
||||
})
|
||||
})
|
||||
|
||||
describe('isProjectReadOnlyFactory returns a function that', () => {
|
||||
it('returns false if project is not in a workspace', async () => {
|
||||
const getWorkspacePlanByProjectId: GetWorkspacePlanByProjectId = async () => null
|
||||
|
||||
const isProjectReadOnly = isProjectReadOnlyFactory({
|
||||
getWorkspacePlanByProjectId
|
||||
})
|
||||
|
||||
expect(await isProjectReadOnly({ projectId: '' })).to.be.false
|
||||
})
|
||||
it('returns true if workspace plan status is expired', async () => {
|
||||
const getWorkspacePlanByProjectId: GetWorkspacePlanByProjectId = async () =>
|
||||
({
|
||||
status: 'expired'
|
||||
} as unknown as ReturnType<GetWorkspacePlanByProjectId>)
|
||||
|
||||
const isProjectReadOnly = isProjectReadOnlyFactory({
|
||||
getWorkspacePlanByProjectId
|
||||
})
|
||||
|
||||
expect(await isProjectReadOnly({ projectId: '' })).to.be.true
|
||||
})
|
||||
it('returns true if workspace plan status is paymentFailed', async () => {
|
||||
const getWorkspacePlanByProjectId: GetWorkspacePlanByProjectId = async () =>
|
||||
({
|
||||
status: 'paymentFailed'
|
||||
} as unknown as ReturnType<GetWorkspacePlanByProjectId>)
|
||||
|
||||
const isProjectReadOnly = isProjectReadOnlyFactory({
|
||||
getWorkspacePlanByProjectId
|
||||
})
|
||||
|
||||
expect(await isProjectReadOnly({ projectId: '' })).to.be.false
|
||||
})
|
||||
it('returns true if workspace plan status is canceled', async () => {
|
||||
const getWorkspacePlanByProjectId: GetWorkspacePlanByProjectId = async () =>
|
||||
({
|
||||
status: 'canceled'
|
||||
} as unknown as ReturnType<GetWorkspacePlanByProjectId>)
|
||||
|
||||
const isProjectReadOnly = isProjectReadOnlyFactory({
|
||||
getWorkspacePlanByProjectId
|
||||
})
|
||||
|
||||
expect(await isProjectReadOnly({ projectId: '' })).to.be.true
|
||||
})
|
||||
it('returns false if workspace plan status is trial', async () => {
|
||||
const getWorkspacePlanByProjectId: GetWorkspacePlanByProjectId = async () =>
|
||||
({
|
||||
status: 'trial'
|
||||
} as unknown as ReturnType<GetWorkspacePlanByProjectId>)
|
||||
|
||||
const isProjectReadOnly = isProjectReadOnlyFactory({
|
||||
getWorkspacePlanByProjectId
|
||||
})
|
||||
|
||||
expect(await isProjectReadOnly({ projectId: '' })).to.be.false
|
||||
})
|
||||
it('returns false if workspace plan status is valid', async () => {
|
||||
const getWorkspacePlanByProjectId: GetWorkspacePlanByProjectId = async () =>
|
||||
({
|
||||
status: 'valid'
|
||||
} as unknown as ReturnType<GetWorkspacePlanByProjectId>)
|
||||
|
||||
const isProjectReadOnly = isProjectReadOnlyFactory({
|
||||
getWorkspacePlanByProjectId
|
||||
})
|
||||
|
||||
expect(await isProjectReadOnly({ projectId: '' })).to.be.false
|
||||
})
|
||||
it('returns false if workspace plan status is cancelationScheduled', async () => {
|
||||
const getWorkspacePlanByProjectId: GetWorkspacePlanByProjectId = async () =>
|
||||
({
|
||||
status: 'cancelationScheduled'
|
||||
} as unknown as ReturnType<GetWorkspacePlanByProjectId>)
|
||||
|
||||
const isProjectReadOnly = isProjectReadOnlyFactory({
|
||||
getWorkspacePlanByProjectId
|
||||
})
|
||||
|
||||
expect(await isProjectReadOnly({ projectId: '' })).to.be.false
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user