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; 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(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; devDependencies?: Record; }; 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('node:http'); const https = jest.requireActual('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(); }); });