From 7bb99df3bf9810aff8e44db5ad5c7d323c35a7c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20Jedlicska?= Date: Fri, 18 Oct 2024 12:03:51 +0200 Subject: [PATCH] test(gatekeeper): add checkout service tests --- .../modules/gatekeeper/services/checkout.ts | 7 +- .../gatekeeper/tests/unit/checkout.spec.ts | 316 ++++++++++++++++++ 2 files changed, 320 insertions(+), 3 deletions(-) create mode 100644 packages/server/modules/gatekeeper/tests/unit/checkout.spec.ts diff --git a/packages/server/modules/gatekeeper/services/checkout.ts b/packages/server/modules/gatekeeper/services/checkout.ts index de5536f19..283f53c02 100644 --- a/packages/server/modules/gatekeeper/services/checkout.ts +++ b/packages/server/modules/gatekeeper/services/checkout.ts @@ -15,6 +15,7 @@ import { WorkspacePlanBillingIntervals } from '@/modules/gatekeeper/domain/workspacePricing' import { + CheckoutSessionNotFoundError, WorkspaceAlreadyPaidError, WorkspaceCheckoutSessionInProgressError } from '@/modules/gatekeeper/errors/billing' @@ -49,6 +50,7 @@ export const startCheckoutSessionFactory = // get workspace plan, if we're already on a paid plan, do not allow checkout // paid plans should use a subscription modification const existingWorkspacePlan = await getWorkspacePlan({ workspaceId }) + // it will technically not be possible to not have if (existingWorkspacePlan) { // maybe we can just ignore the plan not existing, cause we're putting it on a plan post checkout switch (existingWorkspacePlan.status) { @@ -117,13 +119,12 @@ export const completeCheckoutSessionFactory = subscriptionId: string }): Promise => { const checkoutSession = await getCheckoutSession({ sessionId }) - if (!checkoutSession) - throw new Error('checkout session is not found this is a bo bo') + if (!checkoutSession) throw new CheckoutSessionNotFoundError() switch (checkoutSession.paymentStatus) { case 'paid': // if the session is already paid, we do not need to provision anything - return + throw new WorkspaceAlreadyPaidError() case 'unpaid': break default: diff --git a/packages/server/modules/gatekeeper/tests/unit/checkout.spec.ts b/packages/server/modules/gatekeeper/tests/unit/checkout.spec.ts new file mode 100644 index 000000000..7e62b8765 --- /dev/null +++ b/packages/server/modules/gatekeeper/tests/unit/checkout.spec.ts @@ -0,0 +1,316 @@ +import { + CheckoutSessionNotFoundError, + WorkspaceAlreadyPaidError, + WorkspaceCheckoutSessionInProgressError +} from '@/modules/gatekeeper/errors/billing' +import { + completeCheckoutSessionFactory, + startCheckoutSessionFactory +} from '@/modules/gatekeeper/services/checkout' +import { expectToThrow } from '@/test/assertionHelper' +import { expect } from 'chai' +import cryptoRandomString from 'crypto-random-string' +import { + CheckoutSession, + PaidWorkspacePlan, + SubscriptionData, + WorkspaceSubscription +} from '@/modules/gatekeeper/domain/billing' +import { + PaidWorkspacePlans, + WorkspacePlanBillingIntervals +} from '@/modules/gatekeeper/domain/workspacePricing' + +describe('checkout @gatekeeper', () => { + describe('startCheckoutSessionFactory creates a function, that', () => { + it('does not allow checkout for workspace plans, that is in a valid state', async () => { + const workspaceId = cryptoRandomString({ length: 10 }) + const err = await expectToThrow(() => + startCheckoutSessionFactory({ + getWorkspacePlan: async () => ({ + name: 'pro', + status: 'valid', + workspaceId + }), + getWorkspaceCheckoutSession: () => { + expect.fail() + }, + countRole: () => { + expect.fail() + }, + createCheckoutSession: () => { + expect.fail() + }, + saveCheckoutSession: () => { + expect.fail() + } + })({ + workspaceId, + billingInterval: 'monthly', + workspacePlan: 'business', + workspaceSlug: cryptoRandomString({ length: 10 }) + }) + ) + expect(err.message).to.be.equal(new WorkspaceAlreadyPaidError().message) + }) + it('does not allow checkout for workspace plans, that is in a paymentFailed state', async () => { + const workspaceId = cryptoRandomString({ length: 10 }) + const err = await expectToThrow(() => + startCheckoutSessionFactory({ + getWorkspacePlan: async () => ({ + name: 'pro', + status: 'paymentFailed', + workspaceId + }), + getWorkspaceCheckoutSession: () => { + expect.fail() + }, + countRole: () => { + expect.fail() + }, + createCheckoutSession: () => { + expect.fail() + }, + saveCheckoutSession: () => { + expect.fail() + } + })({ + workspaceId, + billingInterval: 'monthly', + workspacePlan: 'business', + workspaceSlug: cryptoRandomString({ length: 10 }) + }) + ) + expect(err.message).to.be.equal(new WorkspaceAlreadyPaidError().message) + }) + it('does not allow checkout for a workspace, that already has a checkout session', async () => { + const workspaceId = cryptoRandomString({ length: 10 }) + const err = await expectToThrow(() => + startCheckoutSessionFactory({ + getWorkspacePlan: async () => ({ + name: 'team', + status: 'trial', + workspaceId + }), + getWorkspaceCheckoutSession: async () => ({ + billingInterval: 'monthly', + id: cryptoRandomString({ length: 10 }), + paymentStatus: 'unpaid', + url: '', + workspaceId, + workspacePlan: 'business' + }), + countRole: () => { + expect.fail() + }, + createCheckoutSession: () => { + expect.fail() + }, + saveCheckoutSession: () => { + expect.fail() + } + })({ + workspaceId, + billingInterval: 'monthly', + workspacePlan: 'business', + workspaceSlug: cryptoRandomString({ length: 10 }) + }) + ) + expect(err.message).to.be.equal( + new WorkspaceCheckoutSessionInProgressError().message + ) + }) + it('creates and stores a checkout for workspaces that are not on a plan', async () => { + const workspaceId = cryptoRandomString({ length: 10 }) + const workspacePlan: PaidWorkspacePlans = 'pro' + const billingInterval: WorkspacePlanBillingIntervals = 'monthly' + const checkoutSession: CheckoutSession = { + id: cryptoRandomString({ length: 10 }), + workspaceId, + workspacePlan, + url: 'https://example.com', + billingInterval, + paymentStatus: 'unpaid' + } + let storedCheckoutSession: CheckoutSession | undefined = undefined + const createdCheckoutSession = await startCheckoutSessionFactory({ + getWorkspacePlan: async () => null, + getWorkspaceCheckoutSession: async () => null, + countRole: async () => 1, + createCheckoutSession: async () => checkoutSession, + saveCheckoutSession: async ({ checkoutSession }) => { + storedCheckoutSession = checkoutSession + } + })({ + workspaceId, + billingInterval, + workspacePlan, + workspaceSlug: cryptoRandomString({ length: 10 }) + }) + expect(checkoutSession).deep.equal(storedCheckoutSession) + expect(checkoutSession).deep.equal(createdCheckoutSession) + }) + it('creates and stores a checkout for TRIAL and CANCELLED workspaces', async () => { + const workspaceId = cryptoRandomString({ length: 10 }) + const workspacePlan: PaidWorkspacePlans = 'pro' + const billingInterval: WorkspacePlanBillingIntervals = 'monthly' + const checkoutSession: CheckoutSession = { + id: cryptoRandomString({ length: 10 }), + workspaceId, + workspacePlan, + url: 'https://example.com', + billingInterval, + paymentStatus: 'unpaid' + } + let storedCheckoutSession: CheckoutSession | undefined = undefined + const createdCheckoutSession = await startCheckoutSessionFactory({ + getWorkspacePlan: async () => null, + getWorkspaceCheckoutSession: async () => null, + countRole: async () => 1, + createCheckoutSession: async () => checkoutSession, + saveCheckoutSession: async ({ checkoutSession }) => { + storedCheckoutSession = checkoutSession + } + })({ + workspaceId, + billingInterval, + workspacePlan, + workspaceSlug: cryptoRandomString({ length: 10 }) + }) + expect(checkoutSession).deep.equal(storedCheckoutSession) + expect(checkoutSession).deep.equal(createdCheckoutSession) + }) + }) + describe('completeCheckoutSessionFactory creates a function, that', () => { + it('throws a CheckoutSessionNotFound if the checkoutSession is null', async () => { + const sessionId = cryptoRandomString({ length: 10 }) + const subscriptionId = cryptoRandomString({ length: 10 }) + + const err = await expectToThrow(async () => { + await completeCheckoutSessionFactory({ + getCheckoutSession: async () => null, + updateCheckoutSessionStatus: async () => { + expect.fail() + }, + upsertPaidWorkspacePlan: async () => { + expect.fail() + }, + getSubscriptionData: async () => { + expect.fail() + }, + saveWorkspaceSubscription: async () => { + expect.fail() + } + })({ sessionId, subscriptionId }) + expect(err.message).to.equal(new CheckoutSessionNotFoundError().message) + }) + }) + it('throws for already paid checkout sessions', async () => { + const sessionId = cryptoRandomString({ length: 10 }) + const subscriptionId = cryptoRandomString({ length: 10 }) + + const err = await expectToThrow(async () => { + await completeCheckoutSessionFactory({ + getCheckoutSession: async () => ({ + billingInterval: 'monthly', + id: sessionId, + paymentStatus: 'paid', + url: 'https://example.com', + workspaceId: cryptoRandomString({ length: 10 }), + workspacePlan: 'business' + }), + updateCheckoutSessionStatus: async () => { + expect.fail() + }, + upsertPaidWorkspacePlan: async () => { + expect.fail() + }, + getSubscriptionData: async () => { + expect.fail() + }, + saveWorkspaceSubscription: async () => { + expect.fail() + } + })({ sessionId, subscriptionId }) + expect(err.message).to.equal(new WorkspaceAlreadyPaidError().message) + }) + }), + (['monthly', 'yearly'] as const).forEach((billingInterval) => { + it(`sets the billingCycleEnd end for ${billingInterval} based on the checkoutSession.billingInterval`, async () => { + const sessionId = cryptoRandomString({ length: 10 }) + const subscriptionId = cryptoRandomString({ length: 10 }) + const workspaceId = cryptoRandomString({ length: 10 }) + + const storedCheckoutSession: CheckoutSession = { + billingInterval, + id: sessionId, + paymentStatus: 'unpaid', + url: 'https://example.com', + workspaceId, + workspacePlan: 'business' + } + + let storedWorkspacePlan: PaidWorkspacePlan | undefined = undefined + + const subscriptionData: SubscriptionData = { + customerId: cryptoRandomString({ length: 10 }), + subscriptionId: cryptoRandomString({ length: 10 }), + products: [ + { + priceId: cryptoRandomString({ length: 10 }), + productId: cryptoRandomString({ length: 10 }), + quantity: 10, + subscriptionItemId: cryptoRandomString({ length: 10 }) + } + ] + } + + let storedWorkspaceSubscriptionData: WorkspaceSubscription | undefined = + undefined + + await completeCheckoutSessionFactory({ + getCheckoutSession: async () => storedCheckoutSession, + updateCheckoutSessionStatus: async ({ paymentStatus }) => { + storedCheckoutSession.paymentStatus = paymentStatus + }, + upsertPaidWorkspacePlan: async ({ workspacePlan }) => { + storedWorkspacePlan = workspacePlan + }, + getSubscriptionData: async () => subscriptionData, + saveWorkspaceSubscription: async ({ workspaceSubscription }) => { + storedWorkspaceSubscriptionData = workspaceSubscription + } + })({ sessionId, subscriptionId }) + + expect(storedCheckoutSession.paymentStatus).to.equal('paid') + expect(storedWorkspacePlan).to.deep.equal({ + workspaceId, + name: storedCheckoutSession.workspacePlan, + status: 'valid' + }) + expect(storedWorkspaceSubscriptionData!.billingInterval).to.equal( + storedCheckoutSession.billingInterval + ) + + expect(storedWorkspaceSubscriptionData!.subscriptionData).to.equal( + subscriptionData + ) + let billingCycleEndsIn: number + const expectedCycleLength = 1 + switch (billingInterval) { + case 'monthly': + billingCycleEndsIn = + storedWorkspaceSubscriptionData!.currentBillingCycleEnd.getMonth() - + new Date().getMonth() + break + case 'yearly': + billingCycleEndsIn = + storedWorkspaceSubscriptionData!.currentBillingCycleEnd.getFullYear() - + new Date().getFullYear() + break + } + expect(billingCycleEndsIn).to.be.equal(expectedCycleLength) + }) + }) + }) +})