feat(billing): implement robust in-app subscription cancellation & fix CI/CD socket port typo
This commit is contained in:
136
memento-note/tests/unit/billing-cancel.test.ts
Normal file
136
memento-note/tests/unit/billing-cancel.test.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Mock the dependencies before importing the function
|
||||
vi.mock('@/lib/prisma', () => ({
|
||||
prisma: {
|
||||
subscription: {
|
||||
findUnique: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/stripe', () => ({
|
||||
stripe: {
|
||||
subscriptions: {
|
||||
update: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
import { cancelSubscription } from '@/lib/billing/cancel-subscription';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
|
||||
describe('cancelSubscription', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return error when userId is missing', async () => {
|
||||
const result = await cancelSubscription('');
|
||||
expect(result).toEqual({ success: false, error: 'User ID is required' });
|
||||
expect(prisma.subscription.findUnique).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return error when no subscription is found in db', async () => {
|
||||
vi.mocked(prisma.subscription.findUnique).mockResolvedValue(null);
|
||||
|
||||
const result = await cancelSubscription('user_123');
|
||||
expect(result).toEqual({ success: false, error: 'No active subscription found' });
|
||||
expect(prisma.subscription.findUnique).toHaveBeenCalledWith({
|
||||
where: { userId: 'user_123' },
|
||||
});
|
||||
expect(stripe.subscriptions.update).not.toHaveBeenCalled();
|
||||
expect(prisma.subscription.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should cancel subscription in Stripe and update DB successfully', async () => {
|
||||
const mockDbSub = {
|
||||
userId: 'user_123',
|
||||
stripeSubscriptionId: 'sub_stripe_123',
|
||||
currentPeriodEnd: new Date(1702678400 * 1000),
|
||||
};
|
||||
const mockStripeSub = {
|
||||
id: 'sub_stripe_123',
|
||||
canceled_at: 1700100000,
|
||||
current_period_end: 1702678400,
|
||||
};
|
||||
|
||||
vi.mocked(prisma.subscription.findUnique).mockResolvedValue(mockDbSub as any);
|
||||
vi.mocked(stripe.subscriptions.update).mockResolvedValue(mockStripeSub as any);
|
||||
vi.mocked(prisma.subscription.update).mockResolvedValue({} as any);
|
||||
|
||||
const result = await cancelSubscription('user_123');
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(stripe.subscriptions.update).toHaveBeenCalledWith('sub_stripe_123', {
|
||||
cancel_at_period_end: true,
|
||||
});
|
||||
expect(prisma.subscription.update).toHaveBeenCalledWith({
|
||||
where: { userId: 'user_123' },
|
||||
data: expect.objectContaining({
|
||||
cancelAtPeriodEnd: true,
|
||||
canceledAt: expect.any(Date),
|
||||
currentPeriodEnd: expect.any(Date),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('should support local / mock mode cancel when stripeSubscriptionId is missing', async () => {
|
||||
const mockDbSub = {
|
||||
userId: 'user_123',
|
||||
stripeSubscriptionId: null,
|
||||
};
|
||||
|
||||
vi.mocked(prisma.subscription.findUnique).mockResolvedValue(mockDbSub as any);
|
||||
vi.mocked(prisma.subscription.update).mockResolvedValue({} as any);
|
||||
|
||||
const result = await cancelSubscription('user_123');
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(stripe.subscriptions.update).not.toHaveBeenCalled();
|
||||
expect(prisma.subscription.update).toHaveBeenCalledWith({
|
||||
where: { userId: 'user_123' },
|
||||
data: expect.objectContaining({
|
||||
cancelAtPeriodEnd: true,
|
||||
canceledAt: expect.any(Date),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error when Stripe API call fails', async () => {
|
||||
const mockDbSub = {
|
||||
userId: 'user_123',
|
||||
stripeSubscriptionId: 'sub_stripe_123',
|
||||
};
|
||||
|
||||
vi.mocked(prisma.subscription.findUnique).mockResolvedValue(mockDbSub as any);
|
||||
vi.mocked(stripe.subscriptions.update).mockRejectedValue(new Error('Stripe connection error'));
|
||||
|
||||
const result = await cancelSubscription('user_123');
|
||||
|
||||
expect(result).toEqual({ success: false, error: 'Stripe connection error' });
|
||||
expect(prisma.subscription.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return error when database write fails', async () => {
|
||||
const mockDbSub = {
|
||||
userId: 'user_123',
|
||||
stripeSubscriptionId: 'sub_stripe_123',
|
||||
};
|
||||
const mockStripeSub = {
|
||||
id: 'sub_stripe_123',
|
||||
canceled_at: 1700100000,
|
||||
current_period_end: 1702678400,
|
||||
};
|
||||
|
||||
vi.mocked(prisma.subscription.findUnique).mockResolvedValue(mockDbSub as any);
|
||||
vi.mocked(stripe.subscriptions.update).mockResolvedValue(mockStripeSub as any);
|
||||
vi.mocked(prisma.subscription.update).mockRejectedValue(new Error('Prisma database error'));
|
||||
|
||||
const result = await cancelSubscription('user_123');
|
||||
|
||||
expect(result).toEqual({ success: false, error: 'Prisma database error' });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user