velody/backend/src/modules/billing/billing.module.spec.ts
2026-06-28 19:50:52 +02:00

138 lines
4.3 KiB
TypeScript

import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { NotImplementedException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import {
BILLING_PROVIDER,
BillingProvider,
} from './billing-provider.interface';
import { BillingProviderService } from './billing-provider.service';
import { BillingModule } from './billing.module';
import { PaddleBillingProvider } from './paddle-billing.provider';
const PROVIDER_METHODS = [
'createCustomer',
'createSubscription',
'updateSubscription',
'cancelSubscription',
'reactivateSubscription',
'getSubscription',
'createCheckoutSession',
'createCustomerPortalSession',
'validateWebhookSignature',
'handleWebhookEvent',
] as const satisfies ReadonlyArray<keyof BillingProvider>;
describe('BillingModule', () => {
let moduleRef: TestingModule;
let provider: PaddleBillingProvider;
let service: BillingProviderService;
beforeAll(async () => {
moduleRef = await Test.createTestingModule({
imports: [BillingModule],
}).compile();
provider = moduleRef.get(PaddleBillingProvider);
service = moduleRef.get(BillingProviderService);
});
afterAll(async () => {
await moduleRef.close();
});
it('registers and exports BillingProviderService', () => {
expect(service).toBeInstanceOf(BillingProviderService);
});
it('implements the BillingProvider contract with PaddleBillingProvider', () => {
const contract: BillingProvider = provider;
expect(contract).toBeInstanceOf(PaddleBillingProvider);
expect(
PROVIDER_METHODS.every(
(method) => typeof contract[method] === 'function',
),
).toBe(true);
});
it('selects PaddleBillingProvider for the internal provider token', () => {
const selectedProvider = moduleRef.get<BillingProvider>(BILLING_PROVIDER);
expect(selectedProvider).toBe(provider);
});
it.each(PROVIDER_METHODS)(
'throws NotImplementedException from provider method %s',
async (method) => {
await expect(provider[method]()).rejects.toBeInstanceOf(
NotImplementedException,
);
},
);
it.each(PROVIDER_METHODS)(
'throws NotImplementedException from service method %s',
async (method) => {
await expect(service[method]()).rejects.toBeInstanceOf(
NotImplementedException,
);
},
);
it('does not import a Paddle SDK or declare one as a dependency', () => {
const sourceFiles = [
'billing-provider.interface.ts',
'billing-provider.service.ts',
'billing.module.ts',
'paddle-billing.provider.ts',
];
const billingSource = sourceFiles
.map((file) => readFileSync(join(__dirname, file), 'utf8'))
.join('\n');
const packageJson = JSON.parse(
readFileSync(join(__dirname, '../../../package.json'), 'utf8'),
) as {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
};
const dependencyNames = [
...Object.keys(packageJson.dependencies ?? {}),
...Object.keys(packageJson.devDependencies ?? {}),
];
const externalImports = [
...billingSource.matchAll(/(?:from\s+|require\()['"]([^'"]+)['"]/g),
]
.map((match) => match[1])
.filter((specifier) => !specifier.startsWith('.'));
expect(externalImports).not.toEqual(
expect.arrayContaining([expect.stringMatching(/paddle/i)]),
);
expect(dependencyNames).not.toEqual(
expect.arrayContaining([expect.stringMatching(/paddle/i)]),
);
});
it('performs no HTTP requests while all operations reject', async () => {
const http = jest.requireActual<typeof import('node:http')>('node:http');
const https = jest.requireActual<typeof import('node:https')>('node:https');
const httpRequest = jest.spyOn(http, 'request');
const httpsRequest = jest.spyOn(https, 'request');
const fetchRequest = jest.spyOn(globalThis, 'fetch');
for (const method of PROVIDER_METHODS) {
await expect(service[method]()).rejects.toBeInstanceOf(
NotImplementedException,
);
}
expect(httpRequest).not.toHaveBeenCalled();
expect(httpsRequest).not.toHaveBeenCalled();
expect(fetchRequest).not.toHaveBeenCalled();
httpRequest.mockRestore();
httpsRequest.mockRestore();
fetchRequest.mockRestore();
});
});