xpeditis2.0/apps/backend/src/application/services/subscription-sync.security.spec.ts

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);
});
});