116 lines
4.6 KiB
TypeScript
116 lines
4.6 KiB
TypeScript
import { ConfigService } from '@nestjs/config';
|
|
import { SubscriptionService } from './subscription.service';
|
|
import { Subscription } from '@domain/entities/subscription.entity';
|
|
import { SubscriptionRepository } from '@domain/ports/out/subscription.repository';
|
|
import { LicenseRepository } from '@domain/ports/out/license.repository';
|
|
import { OrganizationRepository } from '@domain/ports/out/organization.repository';
|
|
import { UserRepository } from '@domain/ports/out/user.repository';
|
|
import {
|
|
StripePort,
|
|
StripeCheckoutSessionData,
|
|
StripeSubscriptionData,
|
|
} from '@domain/ports/out/stripe.port';
|
|
import { SubscriptionOverviewResponseDto } from '../dto/subscription.dto';
|
|
|
|
describe('Stripe checkout organization binding', () => {
|
|
let subscription: Subscription;
|
|
let session: StripeCheckoutSessionData;
|
|
let stripeData: StripeSubscriptionData;
|
|
let save: jest.Mock;
|
|
let getSubscription: jest.Mock;
|
|
let service: SubscriptionService;
|
|
beforeEach(() => {
|
|
subscription = Subscription.create({ id: 'local-sub', organizationId: 'org-caller' });
|
|
session = {
|
|
sessionId: 'cs_fixture',
|
|
customerId: 'cus_fixture',
|
|
subscriptionId: 'sub_fixture',
|
|
status: 'complete',
|
|
metadata: { organizationId: 'org-caller' },
|
|
};
|
|
stripeData = {
|
|
subscriptionId: 'sub_fixture',
|
|
customerId: 'cus_fixture',
|
|
status: 'active',
|
|
planId: 'price_fixture',
|
|
currentPeriodStart: new Date(),
|
|
currentPeriodEnd: new Date(),
|
|
cancelAtPeriodEnd: false,
|
|
};
|
|
// No local row owns the Stripe subscription yet: models checkout before webhook delivery.
|
|
save = jest.fn(async (value: Subscription) => {
|
|
subscription = value;
|
|
return value;
|
|
});
|
|
getSubscription = jest.fn(async () => stripeData);
|
|
service = new SubscriptionService(
|
|
{ findByOrganizationId: async () => subscription, save } as unknown as SubscriptionRepository,
|
|
{ countActiveBySubscriptionIdExcludingAdmins: async () => 0 } as unknown as LicenseRepository,
|
|
{} as OrganizationRepository,
|
|
{} as UserRepository,
|
|
{
|
|
getCheckoutSession: async () => session,
|
|
getSubscription,
|
|
mapPriceIdToPlan: () => 'GOLD',
|
|
} as unknown as StripePort,
|
|
new ConfigService()
|
|
);
|
|
jest
|
|
.spyOn(service, 'getSubscriptionOverview')
|
|
.mockResolvedValue({} as SubscriptionOverviewResponseDto);
|
|
});
|
|
|
|
it.each(['org-victim', undefined])(
|
|
'rejects a checkout whose organization is %j before fetching or saving its subscription',
|
|
async owner => {
|
|
session.metadata = owner ? { organizationId: owner } : {};
|
|
await expect(service.syncFromStripe('org-caller', session.sessionId)).rejects.toMatchObject({
|
|
status: 403,
|
|
});
|
|
expect(getSubscription).not.toHaveBeenCalled();
|
|
expect(save).not.toHaveBeenCalled();
|
|
}
|
|
);
|
|
|
|
it('preserves checkout synchronization for its authenticated organization', async () => {
|
|
await service.syncFromStripe('org-caller', session.sessionId);
|
|
expect(save).toHaveBeenCalledTimes(1);
|
|
expect(subscription.stripeSubscriptionId).toBe('sub_fixture');
|
|
expect(subscription.stripeCustomerId).toBe('cus_fixture');
|
|
expect(subscription.plan.value).toBe('GOLD');
|
|
});
|
|
|
|
it('does not accept a foreign checkout merely because the customer matches', async () => {
|
|
subscription = subscription.updateStripeCustomerId('cus_fixture');
|
|
session.metadata = { organizationId: 'org-victim' };
|
|
await expect(service.syncFromStripe('org-caller', session.sessionId)).rejects.toMatchObject({
|
|
status: 403,
|
|
});
|
|
expect(save).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('permits an owned upgrade to replace the existing Stripe subscription ID', async () => {
|
|
subscription = subscription.updateStripeCustomerId('cus_fixture').updateStripeSubscription({
|
|
stripeSubscriptionId: 'sub_old',
|
|
currentPeriodStart: new Date(),
|
|
currentPeriodEnd: new Date(),
|
|
cancelAtPeriodEnd: false,
|
|
});
|
|
await service.syncFromStripe('org-caller', session.sessionId);
|
|
expect(subscription.stripeSubscriptionId).toBe('sub_fixture');
|
|
expect(save).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('preserves sessionless refresh of an already linked subscription', async () => {
|
|
subscription = subscription.updateStripeCustomerId('cus_fixture').updateStripeSubscription({
|
|
stripeSubscriptionId: 'sub_fixture',
|
|
currentPeriodStart: new Date(),
|
|
currentPeriodEnd: new Date(),
|
|
cancelAtPeriodEnd: false,
|
|
});
|
|
await service.syncFromStripe('org-caller');
|
|
expect(getSubscription).toHaveBeenCalledWith('sub_fixture');
|
|
expect(save).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|