919 lines
37 KiB
TypeScript
919 lines
37 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { readFileSync } from 'fs';
|
|
import { resolve } from 'path';
|
|
import {
|
|
cancelRazorpaySubscriptionBodySchema,
|
|
createCheckoutSessionBodySchema,
|
|
createCustomerPortalSessionBodySchema,
|
|
createRazorpayItemBodySchema,
|
|
createRazorpayOrderBodySchema,
|
|
createRazorpayPlanBodySchema,
|
|
createRazorpaySubscriptionBodySchema,
|
|
createStripePriceBodySchema,
|
|
createStripeProductBodySchema,
|
|
getRazorpayWebhookSetupResponseSchema,
|
|
listStripeCatalogQuerySchema,
|
|
listPaymentCustomersQuerySchema,
|
|
listPaymentTransactionsQuerySchema,
|
|
listStripePricesQuerySchema,
|
|
listStripeProductsQuerySchema,
|
|
listRazorpaySubscriptionsQuerySchema,
|
|
listStripeSubscriptionsQuerySchema,
|
|
paymentEnvironmentParamsSchema,
|
|
pauseRazorpaySubscriptionBodySchema,
|
|
razorpayEnvironmentParamsSchema,
|
|
razorpaySubscriptionParamsSchema,
|
|
getRazorpayConfigResponseSchema,
|
|
getRazorpayStatusResponseSchema,
|
|
razorpayWebhookParamsSchema,
|
|
resumeRazorpaySubscriptionBodySchema,
|
|
syncRazorpayPaymentsResponseSchema,
|
|
updateStripePriceBodySchema,
|
|
updateStripeProductBodySchema,
|
|
upsertStripeConfigBodySchema,
|
|
upsertRazorpayConfigBodySchema,
|
|
updateRazorpayItemBodySchema,
|
|
verifyRazorpayOrderBodySchema,
|
|
verifyRazorpaySubscriptionBodySchema,
|
|
} from '@insforge/shared-schemas';
|
|
|
|
const FAKE_LIVE_SECRET_KEY = 'stripe_live_secret_placeholder';
|
|
|
|
describe('payments route schemas', () => {
|
|
const paymentsRouteSource = readFileSync(
|
|
resolve(__dirname, '../../src/api/routes/payments/index.routes.ts'),
|
|
'utf-8'
|
|
);
|
|
const paymentsApiSchemaSource = readFileSync(
|
|
resolve(__dirname, '../../../packages/shared-schemas/src/payments-api.schema.ts'),
|
|
'utf-8'
|
|
);
|
|
const stripeRouteSource = readFileSync(
|
|
resolve(__dirname, '../../src/api/routes/payments/stripe/index.routes.ts'),
|
|
'utf-8'
|
|
);
|
|
const stripeConfigRouteSource = readFileSync(
|
|
resolve(__dirname, '../../src/api/routes/payments/stripe/config.routes.ts'),
|
|
'utf-8'
|
|
);
|
|
const stripeCatalogRouteSource = readFileSync(
|
|
resolve(__dirname, '../../src/api/routes/payments/stripe/catalog.routes.ts'),
|
|
'utf-8'
|
|
);
|
|
const paymentHelpersSource = readFileSync(
|
|
resolve(__dirname, '../../src/services/payments/helpers.ts'),
|
|
'utf-8'
|
|
);
|
|
const razorpayRouteSource = readFileSync(
|
|
resolve(__dirname, '../../src/api/routes/payments/razorpay/index.routes.ts'),
|
|
'utf-8'
|
|
);
|
|
const razorpayConfigRouteSource = readFileSync(
|
|
resolve(__dirname, '../../src/api/routes/payments/razorpay/config.routes.ts'),
|
|
'utf-8'
|
|
);
|
|
const razorpayCatalogRouteSource = readFileSync(
|
|
resolve(__dirname, '../../src/api/routes/payments/razorpay/catalog.routes.ts'),
|
|
'utf-8'
|
|
);
|
|
const webhooksRouteSource = readFileSync(
|
|
resolve(__dirname, '../../src/api/routes/webhooks/index.routes.ts'),
|
|
'utf-8'
|
|
);
|
|
const razorpayWebhookRouteSource = readFileSync(
|
|
resolve(__dirname, '../../src/api/routes/webhooks/razorpay.routes.ts'),
|
|
'utf-8'
|
|
);
|
|
const razorpayWebhookServiceSource = readFileSync(
|
|
resolve(__dirname, '../../src/services/payments/razorpay/webhook.service.ts'),
|
|
'utf-8'
|
|
);
|
|
const razorpaySyncServiceSource = readFileSync(
|
|
resolve(__dirname, '../../src/services/payments/razorpay/sync.service.ts'),
|
|
'utf-8'
|
|
);
|
|
const razorpayConfigServiceSource = readFileSync(
|
|
resolve(__dirname, '../../src/services/payments/razorpay/config.service.ts'),
|
|
'utf-8'
|
|
);
|
|
const razorpayProviderSource = readFileSync(
|
|
resolve(__dirname, '../../src/providers/payments/razorpay.provider.ts'),
|
|
'utf-8'
|
|
);
|
|
|
|
it('keeps checkout session creation on runtime auth before environment admin routes', () => {
|
|
const adminGuardIndex = stripeRouteSource.indexOf('environmentRouter.use(verifyAdmin)');
|
|
expect(adminGuardIndex).toBeGreaterThan(-1);
|
|
expect(stripeRouteSource).toMatch(
|
|
/environmentRouter\.post\(\s*'\/checkout-sessions'[\s\S]*verifyUser[\s\S]*createCheckoutSessionBodySchema/
|
|
);
|
|
expect(stripeRouteSource.indexOf("'/checkout-sessions'")).toBeLessThan(adminGuardIndex);
|
|
expect(stripeRouteSource).toContain('Checkout session creation requires a user token');
|
|
});
|
|
|
|
it('keeps customer portal session creation on runtime auth before environment admin routes', () => {
|
|
const adminGuardIndex = stripeRouteSource.indexOf('environmentRouter.use(verifyAdmin)');
|
|
expect(adminGuardIndex).toBeGreaterThan(-1);
|
|
expect(stripeRouteSource).toMatch(
|
|
/environmentRouter\.post\(\s*'\/customer-portal-sessions'[\s\S]*verifyUser[\s\S]*createCustomerPortalSessionBodySchema/
|
|
);
|
|
expect(stripeRouteSource.indexOf("'/customer-portal-sessions'")).toBeLessThan(adminGuardIndex);
|
|
expect(stripeRouteSource).toContain('Customer portal session creation requires a user token');
|
|
});
|
|
|
|
it('keeps global admin config routes explicit and admin-guarded', () => {
|
|
expect(stripeRouteSource).toMatch(/router\.get\(\s*'\/status',\s*verifyAdmin/);
|
|
expect(stripeRouteSource).toMatch(/router\.get\(\s*'\/config',\s*verifyAdmin/);
|
|
expect(razorpayRouteSource).toMatch(/router\.get\(\s*'\/status',\s*verifyAdmin/);
|
|
expect(razorpayRouteSource).toMatch(/router\.get\(\s*'\/config',\s*verifyAdmin/);
|
|
expect(paymentsRouteSource).toMatch(/router\.use\(\s*'\/stripe',\s*stripeRouter/);
|
|
expect(stripeRouteSource).toMatch(
|
|
/router\.post\(\s*'\/sync',\s*verifyAdmin[\s\S]*environment: 'all'/
|
|
);
|
|
expect(razorpayRouteSource).toMatch(
|
|
/router\.post\(\s*'\/sync',\s*verifyAdmin[\s\S]*syncAll\('all'\)/
|
|
);
|
|
});
|
|
|
|
it('mounts Stripe environment-scoped payments routes under the Stripe router', () => {
|
|
expect(stripeRouteSource).toContain('const environmentRouter = Router({ mergeParams: true });');
|
|
expect(stripeRouteSource).toContain("router.use('/:environment', environmentRouter)");
|
|
expect(paymentsRouteSource).toContain("router.use('/stripe', stripeRouter)");
|
|
expect(paymentsRouteSource).toContain("router.use('/razorpay', razorpayRouter)");
|
|
expect(paymentsRouteSource).toContain("from './stripe/index.routes.js'");
|
|
expect(paymentsRouteSource).toContain("from './razorpay/index.routes.js'");
|
|
expect(paymentsRouteSource).not.toContain("router.use('/:environment'");
|
|
});
|
|
|
|
it('mounts Razorpay environment-scoped config routes under the Razorpay admin guard', () => {
|
|
const adminGuardIndex = razorpayRouteSource.indexOf('environmentRouter.use(verifyAdmin)');
|
|
expect(adminGuardIndex).toBeGreaterThan(-1);
|
|
expect(razorpayRouteSource).toContain(
|
|
'const environmentRouter = Router({ mergeParams: true });'
|
|
);
|
|
expect(
|
|
razorpayRouteSource.indexOf('environmentRouter.use(razorpayConfigRouter)')
|
|
).toBeGreaterThan(adminGuardIndex);
|
|
expect(razorpayRouteSource).toContain("router.use('/:environment', environmentRouter)");
|
|
expect(razorpayRouteSource).toContain("from './config.routes.js'");
|
|
expect(razorpayRouteSource).not.toContain("router.put('/:environment/config'");
|
|
expect(razorpayRouteSource).not.toContain('paymentEnvironmentSchema');
|
|
});
|
|
|
|
it('mounts Razorpay admin reads under provider-specific services', () => {
|
|
const adminGuardIndex = razorpayRouteSource.indexOf('environmentRouter.use(verifyAdmin)');
|
|
expect(adminGuardIndex).toBeGreaterThan(-1);
|
|
expect(
|
|
razorpayRouteSource.indexOf("environmentRouter.use('/catalog', razorpayCatalogRouter)")
|
|
).toBeGreaterThan(adminGuardIndex);
|
|
expect(razorpayRouteSource.indexOf("'/customers'")).toBeGreaterThan(adminGuardIndex);
|
|
expect(razorpayRouteSource.indexOf("'/transactions'")).toBeGreaterThan(adminGuardIndex);
|
|
expect(
|
|
razorpayRouteSource.match(/environmentRouter\.get\(\s*'\/subscriptions'/)?.index
|
|
).toBeGreaterThan(adminGuardIndex);
|
|
expect(razorpayCatalogRouteSource).toContain('RazorpayCatalogService');
|
|
expect(razorpayRouteSource).toContain('RazorpaySubscriptionService');
|
|
expect(razorpayRouteSource).toContain('PaymentTransactionService');
|
|
expect(razorpayRouteSource).not.toContain('ProjectionService');
|
|
expect(razorpayCatalogRouteSource).toMatch(/catalogService\.listCatalog\(environment\)/);
|
|
expect(razorpayCatalogRouteSource).toMatch(/router\.post\(\s*'\/items'/);
|
|
expect(razorpayCatalogRouteSource).toMatch(/router\.patch\(\s*'\/items\/:itemId'/);
|
|
expect(razorpayCatalogRouteSource).toMatch(/router\.post\(\s*'\/plans'/);
|
|
expect(razorpayRouteSource).toMatch(
|
|
/customerService\.listCustomers\(\{ environment, \.\.\.query \},\s*'razorpay'\)/
|
|
);
|
|
expect(razorpayRouteSource).toMatch(
|
|
/transactionService\.listTransactions\(\s*\{[\s\S]*environment,[\s\S]*\.\.\.query,[\s\S]*\},\s*'razorpay'/
|
|
);
|
|
expect(razorpayRouteSource).toMatch(
|
|
/subscriptionService\.listSubscriptions\(\{ environment, \.\.\.query \}\)/
|
|
);
|
|
});
|
|
|
|
it('mounts Razorpay native checkout flows under user auth before admin routes', () => {
|
|
const adminGuardIndex = razorpayRouteSource.indexOf('environmentRouter.use(verifyAdmin)');
|
|
expect(adminGuardIndex).toBeGreaterThan(-1);
|
|
|
|
for (const route of [
|
|
"'/orders'",
|
|
"'/orders/verify'",
|
|
"'/subscriptions'",
|
|
"'/subscriptions/verify'",
|
|
"'/subscriptions/:subscriptionId/cancel'",
|
|
"'/subscriptions/:subscriptionId/pause'",
|
|
"'/subscriptions/:subscriptionId/resume'",
|
|
]) {
|
|
expect(razorpayRouteSource.indexOf(route)).toBeGreaterThan(-1);
|
|
expect(razorpayRouteSource.indexOf(route)).toBeLessThan(adminGuardIndex);
|
|
}
|
|
|
|
expect(razorpayRouteSource).toMatch(
|
|
/environmentRouter\.post\(\s*'\/orders'[\s\S]*verifyUser[\s\S]*createRazorpayOrderBodySchema/
|
|
);
|
|
expect(razorpayRouteSource).toMatch(
|
|
/environmentRouter\.post\(\s*'\/orders\/verify'[\s\S]*verifyUser[\s\S]*verifyRazorpayOrderBodySchema/
|
|
);
|
|
expect(razorpayRouteSource).toMatch(
|
|
/environmentRouter\.post\(\s*'\/subscriptions'[\s\S]*verifyUser[\s\S]*createRazorpaySubscriptionBodySchema/
|
|
);
|
|
expect(razorpayRouteSource).toMatch(
|
|
/subscriptionService\.createSubscription\(\s*\{[\s\S]*environment,[\s\S]*\.\.\.body,[\s\S]*\},\s*req\.user\s*\)/
|
|
);
|
|
expect(razorpayRouteSource).toMatch(
|
|
/environmentRouter\.post\(\s*'\/subscriptions\/verify'[\s\S]*verifyUser[\s\S]*verifyRazorpaySubscriptionBodySchema/
|
|
);
|
|
expect(razorpayRouteSource).toMatch(
|
|
/environmentRouter\.post\(\s*'\/subscriptions\/:subscriptionId\/cancel'[\s\S]*verifyUser[\s\S]*cancelRazorpaySubscriptionBodySchema/
|
|
);
|
|
expect(razorpayRouteSource).toMatch(
|
|
/environmentRouter\.post\(\s*'\/subscriptions\/:subscriptionId\/pause'[\s\S]*verifyUser[\s\S]*pauseRazorpaySubscriptionBodySchema/
|
|
);
|
|
expect(razorpayRouteSource).toMatch(
|
|
/environmentRouter\.post\(\s*'\/subscriptions\/:subscriptionId\/resume'[\s\S]*verifyUser[\s\S]*resumeRazorpaySubscriptionBodySchema/
|
|
);
|
|
expect(razorpayRouteSource).toContain('RazorpayOrderService');
|
|
expect(razorpayRouteSource).not.toContain('checkout-sessions');
|
|
expect(razorpayRouteSource).not.toContain('payment-links');
|
|
});
|
|
|
|
it('keeps Razorpay route validation schemas in shared-schemas', () => {
|
|
expect(paymentsApiSchemaSource).toContain('razorpayEnvironmentParamsSchema');
|
|
expect(paymentsApiSchemaSource).toContain('razorpayWebhookParamsSchema');
|
|
expect(paymentsApiSchemaSource).toContain('upsertRazorpayConfigBodySchema');
|
|
expect(paymentsApiSchemaSource).toContain('getRazorpayWebhookSetupResponseSchema');
|
|
expect(paymentsApiSchemaSource).toContain('rotateRazorpayWebhookSecretResponseSchema');
|
|
expect(paymentsApiSchemaSource).toContain('getRazorpayStatusResponseSchema');
|
|
expect(paymentsApiSchemaSource).toContain('getRazorpayConfigResponseSchema');
|
|
expect(paymentsApiSchemaSource).toContain('syncRazorpayPaymentsResponseSchema');
|
|
expect(paymentsApiSchemaSource).toContain('createRazorpayOrderBodySchema');
|
|
expect(paymentsApiSchemaSource).toContain('verifyRazorpayOrderBodySchema');
|
|
expect(paymentsApiSchemaSource).toContain('createRazorpaySubscriptionBodySchema');
|
|
expect(paymentsApiSchemaSource).toContain('verifyRazorpaySubscriptionBodySchema');
|
|
expect(paymentsApiSchemaSource).toContain('cancelRazorpaySubscriptionBodySchema');
|
|
expect(paymentsApiSchemaSource).toContain('pauseRazorpaySubscriptionBodySchema');
|
|
expect(paymentsApiSchemaSource).toContain('resumeRazorpaySubscriptionBodySchema');
|
|
expect(paymentsApiSchemaSource).toContain('createRazorpayItemBodySchema');
|
|
expect(paymentsApiSchemaSource).toContain('createRazorpayPlanBodySchema');
|
|
expect(razorpayConfigRouteSource).toContain('parseZodSchema');
|
|
expect(razorpayConfigRouteSource).toContain('upsertRazorpayConfigBodySchema');
|
|
expect(razorpayRouteSource).toContain('createRazorpayOrderBodySchema');
|
|
expect(razorpayCatalogRouteSource).toContain('createRazorpayItemBodySchema');
|
|
expect(razorpayCatalogRouteSource).toContain('createRazorpayPlanBodySchema');
|
|
expect(razorpayConfigRouteSource).not.toContain("from 'zod'");
|
|
expect(razorpayConfigRouteSource).not.toContain('paymentEnvironmentSchema');
|
|
expect(razorpayConfigRouteSource).not.toContain('function getEnvironment');
|
|
});
|
|
|
|
it('keeps environment-scoped config, catalog, and admin reads behind the environment admin guard', () => {
|
|
const adminGuardIndex = stripeRouteSource.indexOf('environmentRouter.use(verifyAdmin)');
|
|
expect(adminGuardIndex).toBeGreaterThan(-1);
|
|
expect(stripeRouteSource.indexOf('environmentRouter.use(stripeConfigRouter)')).toBeGreaterThan(
|
|
adminGuardIndex
|
|
);
|
|
expect(
|
|
stripeRouteSource.indexOf("environmentRouter.use('/catalog', stripeCatalogRouter)")
|
|
).toBeGreaterThan(adminGuardIndex);
|
|
expect(stripeRouteSource.indexOf("'/transactions'")).toBeGreaterThan(adminGuardIndex);
|
|
expect(stripeRouteSource.indexOf("'/subscriptions'")).toBeGreaterThan(adminGuardIndex);
|
|
expect(stripeRouteSource.indexOf("'/customers'")).toBeGreaterThan(adminGuardIndex);
|
|
expect(stripeRouteSource).toMatch(
|
|
/environmentRouter\.get\(\s*'\/customers'[\s\S]*listPaymentCustomersQuerySchema[\s\S]*listCustomers/
|
|
);
|
|
});
|
|
|
|
it('keeps environment-scoped config routes in the dedicated Stripe config router', () => {
|
|
expect(stripeConfigRouteSource).toContain('const router = Router({ mergeParams: true });');
|
|
expect(stripeConfigRouteSource).toMatch(
|
|
/router\.put\(\s*'\/config'[\s\S]*upsertStripeConfigBodySchema/
|
|
);
|
|
expect(stripeConfigRouteSource).toMatch(/router\.delete\(\s*'\/config'/);
|
|
expect(stripeConfigRouteSource).toMatch(/router\.post\(\s*'\/sync'/);
|
|
expect(stripeConfigRouteSource).toMatch(/router\.post\(\s*'\/webhook'/);
|
|
expect(stripeConfigRouteSource).toMatch(/export \{ router as stripeConfigRouter \}/);
|
|
expect(stripeConfigRouteSource).not.toMatch(/router\.get\(\s*'\/status'/);
|
|
expect(stripeConfigRouteSource).not.toMatch(/router\.get\(\s*'\/config'/);
|
|
});
|
|
|
|
it('uses the shared payments helper for environment path params', () => {
|
|
expect(paymentHelpersSource).toContain('getPaymentEnvironment');
|
|
expect(paymentHelpersSource).toContain('paymentEnvironmentParamsSchema');
|
|
expect(paymentHelpersSource).toContain('ERROR_CODES.INVALID_INPUT');
|
|
expect(stripeRouteSource).toContain("from '@/services/payments/helpers.js'");
|
|
expect(stripeConfigRouteSource).toContain("from '@/services/payments/helpers.js'");
|
|
expect(stripeCatalogRouteSource).toContain("from '@/services/payments/helpers.js'");
|
|
expect(razorpayRouteSource).toContain("from '@/services/payments/helpers.js'");
|
|
expect(razorpayCatalogRouteSource).toContain("from '@/services/payments/helpers.js'");
|
|
expect(stripeRouteSource).not.toContain('function getEnvironment');
|
|
expect(stripeConfigRouteSource).not.toContain('function getEnvironment');
|
|
expect(stripeCatalogRouteSource).not.toContain('function getEnvironment');
|
|
expect(razorpayRouteSource).not.toContain('function getEnvironment');
|
|
expect(razorpayCatalogRouteSource).not.toContain('function getEnvironment');
|
|
expect(stripeRouteSource).not.toContain("from './route-params.js'");
|
|
expect(stripeConfigRouteSource).not.toContain("from './route-params.js'");
|
|
expect(stripeCatalogRouteSource).not.toContain("from './route-params.js'");
|
|
});
|
|
|
|
it('keeps products and prices consolidated in the Stripe catalog router', () => {
|
|
expect(stripeCatalogRouteSource).toMatch(
|
|
/router\.get\(\s*'\/'[\s\S]*productService\.listProducts[\s\S]*priceService\.listPrices/
|
|
);
|
|
expect(stripeCatalogRouteSource).toMatch(/router\.get\(\s*'\/products'/);
|
|
expect(stripeCatalogRouteSource).toMatch(/router\.get\(\s*'\/prices'/);
|
|
expect(stripeCatalogRouteSource).toMatch(
|
|
/router\.post\(\s*'\/products'[\s\S]*createStripeProductBodySchema/
|
|
);
|
|
expect(stripeCatalogRouteSource).toMatch(
|
|
/router\.post\(\s*'\/prices'[\s\S]*createStripePriceBodySchema/
|
|
);
|
|
expect(stripeCatalogRouteSource).toMatch(/export \{ router as stripeCatalogRouter \}/);
|
|
expect(stripeCatalogRouteSource).not.toContain('products.routes');
|
|
expect(stripeCatalogRouteSource).not.toContain('prices.routes');
|
|
});
|
|
|
|
it('keeps Razorpay admin webhook setup aligned with the Stripe admin route shape', () => {
|
|
expect(razorpayConfigRouteSource).toContain('const router = Router({ mergeParams: true });');
|
|
expect(razorpayConfigRouteSource).toMatch(/router\.put\(\s*'\/config'/);
|
|
expect(razorpayConfigRouteSource).toMatch(/router\.delete\(\s*'\/config'/);
|
|
expect(razorpayConfigRouteSource).toMatch(/router\.post\(\s*'\/sync'/);
|
|
expect(razorpayConfigRouteSource).toMatch(/router\.get\(\s*'\/webhook'/);
|
|
expect(razorpayConfigRouteSource).toContain('configService.getWebhookSetup(environment)');
|
|
expect(razorpayConfigRouteSource).toMatch(/router\.post\(\s*'\/webhook\/rotate-secret'/);
|
|
expect(razorpayConfigRouteSource).toContain('configService.rotateWebhookSecret(environment)');
|
|
expect(razorpayConfigRouteSource).not.toMatch(/router\.post\(\s*'\/webhook'/);
|
|
expect(razorpayConfigRouteSource).toContain('successResponse(res, result)');
|
|
expect(razorpayRouteSource).not.toContain('webhook-configure');
|
|
expect(razorpayConfigRouteSource).not.toContain('webhook-configure');
|
|
expect(razorpayConfigRouteSource).not.toContain('/webhook-secret');
|
|
expect(razorpayConfigRouteSource).not.toContain('verifyAdmin');
|
|
expect(razorpayRouteSource).not.toContain('x-razorpay-signature');
|
|
expect(razorpayRouteSource).not.toContain('verifyWebhookSignature');
|
|
expect(razorpayConfigRouteSource).toMatch(/export \{ router as razorpayConfigRouter \}/);
|
|
});
|
|
|
|
it('keeps Razorpay sync aligned with Stripe provider-level sync shape', () => {
|
|
expect(razorpayRouteSource).toMatch(/router\.post\(\s*'\/sync',\s*verifyAdmin/);
|
|
expect(razorpayConfigRouteSource).toMatch(/router\.post\(\s*'\/sync'/);
|
|
expect(razorpayConfigRouteSource).toContain('syncService.syncAll(environment)');
|
|
expect(razorpayConfigRouteSource).toContain('syncService.syncEnvironmentAfterKeyChange');
|
|
expect(razorpayConfigRouteSource).not.toContain("result.status === 'failed'");
|
|
expect(razorpaySyncServiceSource).toContain('withPaymentSessionAdvisoryLock');
|
|
expect(razorpaySyncServiceSource).toContain('payments_razorpay_environment_');
|
|
expect(razorpaySyncServiceSource).toContain('SyncRazorpayPaymentsResponse');
|
|
expect(razorpaySyncServiceSource).not.toContain('syncPromises');
|
|
});
|
|
|
|
it('keeps Razorpay webhooks manual rather than half-managed through a provider API', () => {
|
|
expect(razorpayConfigRouteSource).toContain('getWebhookSetup(environment)');
|
|
expect(razorpayConfigRouteSource).toContain('rotateWebhookSecret(environment)');
|
|
expect(razorpayProviderSource).not.toContain('createWebhook(');
|
|
expect(razorpayProviderSource).not.toContain('/v1/accounts/me/webhooks');
|
|
expect(razorpayConfigServiceSource).toContain('manual');
|
|
expect(razorpayConfigServiceSource).toContain('webhookSecret');
|
|
expect(razorpayConfigServiceSource).not.toContain('manualSetupRequired');
|
|
});
|
|
|
|
it('mounts Razorpay inbound webhooks under the dedicated webhook router', () => {
|
|
expect(webhooksRouteSource).toContain("router.use('/stripe', stripeWebhookRouter)");
|
|
expect(webhooksRouteSource).toContain("router.use('/razorpay', razorpayWebhookRouter)");
|
|
expect(razorpayWebhookRouteSource).toMatch(/router\.post\(\s*'\/:environment'/);
|
|
expect(razorpayWebhookRouteSource).toContain('x-razorpay-signature');
|
|
expect(razorpayWebhookRouteSource).toContain('razorpayWebhookParamsSchema');
|
|
expect(razorpayWebhookRouteSource).toContain('handleRazorpayWebhook');
|
|
expect(razorpayWebhookRouteSource).not.toContain('HANDLED_RAZORPAY_EVENTS');
|
|
expect(razorpayWebhookRouteSource).not.toContain('parseRazorpayWebhookPayload');
|
|
expect(razorpayWebhookRouteSource).not.toContain('getRazorpayPayloadEntityId');
|
|
expect(razorpayWebhookRouteSource).not.toContain('isRecord');
|
|
expect(razorpayWebhookRouteSource).not.toContain('RazorpayConfigService');
|
|
expect(razorpayWebhookRouteSource).not.toContain('RazorpaySyncService');
|
|
expect(razorpayWebhookRouteSource).not.toContain('logger');
|
|
expect(razorpayWebhookRouteSource).not.toContain('verifyWebhookSignature');
|
|
expect(razorpayWebhookRouteSource).not.toContain('recordWebhookEventStart');
|
|
expect(razorpayWebhookServiceSource).toContain('HANDLED_RAZORPAY_EVENTS');
|
|
expect(razorpayWebhookServiceSource).toContain('verifyWebhookSignature');
|
|
expect(razorpayWebhookServiceSource).toContain('recordWebhookEventStart');
|
|
expect(razorpayWebhookRouteSource).toMatch(/export \{ router as razorpayWebhookRouter \}/);
|
|
});
|
|
|
|
it('accepts only the supported payment environment path params', () => {
|
|
expect(paymentEnvironmentParamsSchema.parse({ environment: 'test' })).toEqual({
|
|
environment: 'test',
|
|
});
|
|
expect(paymentEnvironmentParamsSchema.parse({ environment: 'live' })).toEqual({
|
|
environment: 'live',
|
|
});
|
|
expect(() => paymentEnvironmentParamsSchema.parse({ environment: 'prod' })).toThrow();
|
|
});
|
|
|
|
it('accepts Razorpay config and webhook request schemas from shared-schemas', () => {
|
|
expect(razorpayEnvironmentParamsSchema.parse({ environment: 'test' })).toEqual({
|
|
environment: 'test',
|
|
});
|
|
expect(razorpayWebhookParamsSchema.parse({ environment: 'live' })).toEqual({
|
|
environment: 'live',
|
|
});
|
|
expect(() => razorpayEnvironmentParamsSchema.parse({ environment: 'prod' })).toThrow();
|
|
expect(
|
|
upsertRazorpayConfigBodySchema.parse({
|
|
keyId: 'rzp_test_abc',
|
|
keySecret: 'secret',
|
|
webhookSecret: 'webhook_secret',
|
|
})
|
|
).toEqual({
|
|
keyId: 'rzp_test_abc',
|
|
keySecret: 'secret',
|
|
webhookSecret: 'webhook_secret',
|
|
});
|
|
expect(() =>
|
|
upsertRazorpayConfigBodySchema.parse({ keyId: '', keySecret: 'secret' })
|
|
).toThrow();
|
|
expect(
|
|
getRazorpayStatusResponseSchema.parse({
|
|
razorpayConnections: [
|
|
{
|
|
environment: 'test',
|
|
status: 'unconfigured',
|
|
accountId: null,
|
|
merchantName: null,
|
|
accountLivemode: null,
|
|
webhookEndpointId: null,
|
|
webhookEndpointUrl: null,
|
|
webhookConfiguredAt: null,
|
|
maskedKey: null,
|
|
lastSyncedAt: null,
|
|
lastSyncStatus: null,
|
|
lastSyncError: null,
|
|
lastSyncCounts: {},
|
|
},
|
|
],
|
|
})
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
razorpayConnections: expect.any(Array),
|
|
})
|
|
);
|
|
expect(
|
|
getRazorpayConfigResponseSchema.parse({
|
|
keys: [
|
|
{
|
|
environment: 'test',
|
|
keyType: 'api_key',
|
|
value: 'rzp_test_key1234',
|
|
},
|
|
],
|
|
})
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
keys: expect.any(Array),
|
|
})
|
|
);
|
|
expect(
|
|
getRazorpayWebhookSetupResponseSchema.parse({
|
|
connection: {
|
|
environment: 'test',
|
|
status: 'connected',
|
|
accountId: 'acc_123',
|
|
merchantName: 'Example Merchant',
|
|
accountLivemode: false,
|
|
webhookEndpointId: 'manual',
|
|
webhookEndpointUrl: 'https://api.example.com/api/webhooks/razorpay/test',
|
|
webhookConfiguredAt: null,
|
|
maskedKey: 'rzp_test_****1234',
|
|
lastSyncedAt: null,
|
|
lastSyncStatus: null,
|
|
lastSyncError: null,
|
|
lastSyncCounts: {},
|
|
},
|
|
webhookUrl: 'https://api.example.com/api/webhooks/razorpay/test',
|
|
webhookSecret: 'manual_secret',
|
|
})
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
webhookUrl: 'https://api.example.com/api/webhooks/razorpay/test',
|
|
webhookSecret: 'manual_secret',
|
|
})
|
|
);
|
|
expect(
|
|
syncRazorpayPaymentsResponseSchema.parse({
|
|
results: [
|
|
{
|
|
environment: 'test',
|
|
status: 'failed',
|
|
connection: {
|
|
environment: 'test',
|
|
status: 'unconfigured',
|
|
accountId: null,
|
|
merchantName: null,
|
|
accountLivemode: null,
|
|
webhookEndpointId: null,
|
|
webhookEndpointUrl: null,
|
|
webhookConfiguredAt: null,
|
|
maskedKey: null,
|
|
lastSyncedAt: null,
|
|
lastSyncStatus: 'failed',
|
|
lastSyncError: 'missing keys',
|
|
lastSyncCounts: {},
|
|
},
|
|
syncCounts: {
|
|
plans: 0,
|
|
items: 0,
|
|
customers: 0,
|
|
subscriptions: 0,
|
|
invoices: 0,
|
|
payments: 0,
|
|
},
|
|
error: 'missing keys',
|
|
},
|
|
],
|
|
})
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
results: expect.any(Array),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('accepts empty catalog and products query strings for environment-scoped reads', () => {
|
|
expect(listStripeCatalogQuerySchema.parse({})).toEqual({});
|
|
expect(listStripeProductsQuerySchema.parse({})).toEqual({});
|
|
expect(() => listStripeCatalogQuerySchema.parse({ environment: 'test' })).toThrow();
|
|
});
|
|
|
|
it('accepts Stripe key configuration bodies without embedding environment in the body', () => {
|
|
expect(
|
|
upsertStripeConfigBodySchema.parse({
|
|
secretKey: FAKE_LIVE_SECRET_KEY,
|
|
})
|
|
).toEqual({
|
|
secretKey: FAKE_LIVE_SECRET_KEY,
|
|
});
|
|
expect(() => upsertStripeConfigBodySchema.parse({ secretKey: '' })).toThrow();
|
|
expect(() =>
|
|
upsertStripeConfigBodySchema.parse({
|
|
environment: 'live',
|
|
secretKey: FAKE_LIVE_SECRET_KEY,
|
|
})
|
|
).toThrow();
|
|
});
|
|
|
|
it('accepts product CRUD bodies without embedding environment in the body', () => {
|
|
expect(
|
|
createStripeProductBodySchema.parse({
|
|
name: 'Pro',
|
|
description: null,
|
|
active: true,
|
|
metadata: { tier: 'pro' },
|
|
idempotencyKey: 'agent-product-123',
|
|
})
|
|
).toEqual({
|
|
name: 'Pro',
|
|
description: null,
|
|
active: true,
|
|
metadata: { tier: 'pro' },
|
|
idempotencyKey: 'agent-product-123',
|
|
});
|
|
|
|
expect(() =>
|
|
createStripeProductBodySchema.parse({ name: 'Pro', environment: 'test' })
|
|
).toThrow();
|
|
expect(() =>
|
|
createStripeProductBodySchema.parse({
|
|
name: 'Pro',
|
|
idempotencyKey: 'x'.repeat(201),
|
|
})
|
|
).toThrow(/200 characters/i);
|
|
expect(() => updateStripeProductBodySchema.parse({})).toThrow();
|
|
expect(updateStripeProductBodySchema.parse({ active: false })).toEqual({
|
|
active: false,
|
|
});
|
|
expect(() => updateStripeProductBodySchema.parse({ environment: 'live' })).toThrow();
|
|
});
|
|
|
|
it('accepts price CRUD bodies and query filters without embedding environment in the body', () => {
|
|
expect(listStripePricesQuerySchema.parse({ productId: 'prod_123' })).toEqual({
|
|
productId: 'prod_123',
|
|
});
|
|
expect(
|
|
createStripePriceBodySchema.parse({
|
|
productId: 'prod_123',
|
|
currency: 'USD',
|
|
unitAmount: 2000,
|
|
recurring: { interval: 'month', intervalCount: 1 },
|
|
idempotencyKey: 'agent-price-123',
|
|
})
|
|
).toEqual({
|
|
productId: 'prod_123',
|
|
currency: 'usd',
|
|
unitAmount: 2000,
|
|
recurring: { interval: 'month', intervalCount: 1 },
|
|
idempotencyKey: 'agent-price-123',
|
|
});
|
|
expect(() =>
|
|
createStripePriceBodySchema.parse({
|
|
productId: 'prod_123',
|
|
currency: 'usd',
|
|
unitAmount: 2000,
|
|
environment: 'test',
|
|
})
|
|
).toThrow();
|
|
expect(() => updateStripePriceBodySchema.parse({})).toThrow();
|
|
expect(updateStripePriceBodySchema.parse({ active: false })).toEqual({ active: false });
|
|
expect(() => updateStripePriceBodySchema.parse({ environment: 'live' })).toThrow();
|
|
});
|
|
|
|
it('accepts Razorpay native order and subscription checkout bodies', () => {
|
|
expect(
|
|
createRazorpayOrderBodySchema.parse({
|
|
amount: 50000,
|
|
currency: 'inr',
|
|
receipt: 'order_123',
|
|
subject: { type: 'team', id: 'team_123' },
|
|
customerEmail: 'buyer@example.com',
|
|
})
|
|
).toEqual({
|
|
amount: 50000,
|
|
currency: 'INR',
|
|
receipt: 'order_123',
|
|
subject: { type: 'team', id: 'team_123' },
|
|
customerEmail: 'buyer@example.com',
|
|
});
|
|
|
|
expect(
|
|
verifyRazorpayOrderBodySchema.parse({
|
|
orderId: 'order_123',
|
|
paymentId: 'pay_123',
|
|
signature: 'signature',
|
|
})
|
|
).toEqual({
|
|
orderId: 'order_123',
|
|
paymentId: 'pay_123',
|
|
signature: 'signature',
|
|
});
|
|
|
|
expect(
|
|
createRazorpaySubscriptionBodySchema.parse({
|
|
planId: 'plan_123',
|
|
totalCount: 12,
|
|
subject: { type: 'team', id: 'team_123' },
|
|
customerContact: '+919999999999',
|
|
})
|
|
).toEqual({
|
|
planId: 'plan_123',
|
|
totalCount: 12,
|
|
subject: { type: 'team', id: 'team_123' },
|
|
customerContact: '+919999999999',
|
|
});
|
|
|
|
expect(() =>
|
|
createRazorpaySubscriptionBodySchema.parse({
|
|
planId: 'plan_123',
|
|
subject: { type: 'team', id: 'team_123' },
|
|
})
|
|
).toThrow(/totalCount or endAt/i);
|
|
|
|
expect(
|
|
verifyRazorpaySubscriptionBodySchema.parse({
|
|
subscriptionId: 'sub_123',
|
|
paymentId: 'pay_123',
|
|
signature: 'signature',
|
|
})
|
|
).toEqual({
|
|
subscriptionId: 'sub_123',
|
|
paymentId: 'pay_123',
|
|
signature: 'signature',
|
|
});
|
|
|
|
expect(cancelRazorpaySubscriptionBodySchema.parse({})).toEqual({
|
|
cancelAtCycleEnd: false,
|
|
});
|
|
expect(cancelRazorpaySubscriptionBodySchema.parse({ cancelAtCycleEnd: true })).toEqual({
|
|
cancelAtCycleEnd: true,
|
|
});
|
|
expect(pauseRazorpaySubscriptionBodySchema.parse({})).toEqual({});
|
|
expect(resumeRazorpaySubscriptionBodySchema.parse({})).toEqual({});
|
|
expect(() => pauseRazorpaySubscriptionBodySchema.parse({ planId: 'plan_456' })).toThrow();
|
|
expect(
|
|
razorpaySubscriptionParamsSchema.parse({
|
|
environment: 'test',
|
|
subscriptionId: 'sub_123',
|
|
})
|
|
).toEqual({
|
|
environment: 'test',
|
|
subscriptionId: 'sub_123',
|
|
});
|
|
});
|
|
|
|
it('accepts Razorpay item and plan mutation bodies without pretending plans are editable', () => {
|
|
expect(
|
|
createRazorpayItemBodySchema.parse({
|
|
name: 'Invoice item',
|
|
amount: 25000,
|
|
currency: 'inr',
|
|
})
|
|
).toEqual({
|
|
name: 'Invoice item',
|
|
amount: 25000,
|
|
currency: 'INR',
|
|
});
|
|
|
|
expect(updateRazorpayItemBodySchema.parse({ active: false })).toEqual({ active: false });
|
|
expect(() => updateRazorpayItemBodySchema.parse({})).toThrow();
|
|
|
|
expect(
|
|
createRazorpayPlanBodySchema.parse({
|
|
period: 'monthly',
|
|
interval: 1,
|
|
item: {
|
|
name: 'Pro monthly',
|
|
amount: 199900,
|
|
currency: 'inr',
|
|
},
|
|
notes: { tier: 'pro' },
|
|
})
|
|
).toEqual({
|
|
period: 'monthly',
|
|
interval: 1,
|
|
item: {
|
|
name: 'Pro monthly',
|
|
amount: 199900,
|
|
currency: 'INR',
|
|
},
|
|
notes: { tier: 'pro' },
|
|
});
|
|
|
|
expect(razorpayCatalogRouteSource).not.toMatch(/router\.patch\(\s*'\/plans/);
|
|
expect(razorpayCatalogRouteSource).not.toMatch(/router\.delete\(\s*'\/plans/);
|
|
});
|
|
|
|
it('rejects caller-provided InsForge-reserved Razorpay notes', () => {
|
|
expect(() =>
|
|
createRazorpayOrderBodySchema.parse({
|
|
amount: 50000,
|
|
currency: 'INR',
|
|
notes: {
|
|
insforge_subject_type: 'team',
|
|
},
|
|
})
|
|
).toThrow(/reserved/i);
|
|
|
|
expect(() =>
|
|
createRazorpayPlanBodySchema.parse({
|
|
period: 'monthly',
|
|
interval: 1,
|
|
item: {
|
|
name: 'Pro monthly',
|
|
amount: 199900,
|
|
currency: 'INR',
|
|
},
|
|
notes: {
|
|
insforge_subject_id: 'team_123',
|
|
},
|
|
})
|
|
).toThrow(/reserved/i);
|
|
});
|
|
|
|
it('allows anonymous one-time checkout sessions without embedding environment in the body', () => {
|
|
expect(
|
|
createCheckoutSessionBodySchema.parse({
|
|
mode: 'payment',
|
|
lineItems: [{ priceId: 'price_123', quantity: 2 }],
|
|
successUrl: 'https://example.com/success',
|
|
cancelUrl: 'https://example.com/cancel',
|
|
customerEmail: 'buyer@example.com',
|
|
idempotencyKey: 'checkout-123',
|
|
})
|
|
).toEqual({
|
|
mode: 'payment',
|
|
lineItems: [{ priceId: 'price_123', quantity: 2 }],
|
|
successUrl: 'https://example.com/success',
|
|
cancelUrl: 'https://example.com/cancel',
|
|
customerEmail: 'buyer@example.com',
|
|
idempotencyKey: 'checkout-123',
|
|
});
|
|
});
|
|
|
|
it('rejects caller-provided InsForge-reserved checkout metadata', () => {
|
|
expect(() =>
|
|
createCheckoutSessionBodySchema.parse({
|
|
mode: 'payment',
|
|
lineItems: [{ priceId: 'price_123' }],
|
|
successUrl: 'https://example.com/success',
|
|
cancelUrl: 'https://example.com/cancel',
|
|
metadata: {
|
|
insforge_subject_type: 'team',
|
|
insforge_subject_id: 'team_victim',
|
|
},
|
|
})
|
|
).toThrow(/reserved/i);
|
|
});
|
|
|
|
it('requires subscription checkout sessions to specify a billing subject', () => {
|
|
expect(() =>
|
|
createCheckoutSessionBodySchema.parse({
|
|
mode: 'subscription',
|
|
lineItems: [{ priceId: 'price_123' }],
|
|
successUrl: 'https://example.com/success',
|
|
cancelUrl: 'https://example.com/cancel',
|
|
})
|
|
).toThrow(/billing subject/i);
|
|
|
|
expect(
|
|
createCheckoutSessionBodySchema.parse({
|
|
mode: 'subscription',
|
|
lineItems: [{ priceId: 'price_123' }],
|
|
successUrl: 'https://example.com/success',
|
|
cancelUrl: 'https://example.com/cancel',
|
|
subject: { type: 'team', id: 'team_123' },
|
|
})
|
|
).toEqual({
|
|
mode: 'subscription',
|
|
lineItems: [{ priceId: 'price_123', quantity: 1 }],
|
|
successUrl: 'https://example.com/success',
|
|
cancelUrl: 'https://example.com/cancel',
|
|
subject: { type: 'team', id: 'team_123' },
|
|
});
|
|
});
|
|
|
|
it('requires customer portal session bodies to specify a billing subject without embedding environment', () => {
|
|
expect(
|
|
createCustomerPortalSessionBodySchema.parse({
|
|
subject: { type: 'team', id: 'team_123' },
|
|
returnUrl: 'https://example.com/account',
|
|
configuration: 'bpc_123',
|
|
})
|
|
).toEqual({
|
|
subject: { type: 'team', id: 'team_123' },
|
|
returnUrl: 'https://example.com/account',
|
|
configuration: 'bpc_123',
|
|
});
|
|
|
|
expect(() =>
|
|
createCustomerPortalSessionBodySchema.parse({
|
|
returnUrl: 'https://example.com/account',
|
|
})
|
|
).toThrow();
|
|
expect(() =>
|
|
createCustomerPortalSessionBodySchema.parse({
|
|
subject: { type: 'team', id: 'team_123' },
|
|
returnUrl: 'not-a-url',
|
|
})
|
|
).toThrow(/valid URL/i);
|
|
expect(() =>
|
|
createCustomerPortalSessionBodySchema.parse({
|
|
environment: 'test',
|
|
subject: { type: 'team', id: 'team_123' },
|
|
})
|
|
).toThrow();
|
|
});
|
|
|
|
it('requires runtime list query filters to omit environment and keep complete subject filters', () => {
|
|
expect(listPaymentTransactionsQuerySchema.parse({})).toEqual({ limit: 50 });
|
|
expect(
|
|
listStripeSubscriptionsQuerySchema.parse({
|
|
subjectType: 'organization',
|
|
subjectId: 'org_123',
|
|
limit: '25',
|
|
})
|
|
).toEqual({
|
|
subjectType: 'organization',
|
|
subjectId: 'org_123',
|
|
limit: 25,
|
|
});
|
|
expect(
|
|
listRazorpaySubscriptionsQuerySchema.parse({
|
|
subjectType: 'organization',
|
|
subjectId: 'org_123',
|
|
limit: '25',
|
|
})
|
|
).toEqual({
|
|
subjectType: 'organization',
|
|
subjectId: 'org_123',
|
|
limit: 25,
|
|
});
|
|
|
|
expect(() => listPaymentTransactionsQuerySchema.parse({ environment: 'live' })).toThrow();
|
|
expect(() => listStripeSubscriptionsQuerySchema.parse({ subjectType: 'team' })).toThrow(
|
|
/provided together/i
|
|
);
|
|
expect(() => listRazorpaySubscriptionsQuerySchema.parse({ subjectType: 'team' })).toThrow(
|
|
/provided together/i
|
|
);
|
|
});
|
|
|
|
it('requires admin customer mirror reads to omit environment from the query and normalize limit', () => {
|
|
expect(
|
|
listPaymentCustomersQuerySchema.parse({
|
|
limit: '25',
|
|
})
|
|
).toEqual({
|
|
limit: 25,
|
|
});
|
|
|
|
expect(listPaymentCustomersQuerySchema.parse({})).toEqual({
|
|
limit: 50,
|
|
});
|
|
|
|
expect(() => listPaymentCustomersQuerySchema.parse({ environment: 'test' })).toThrow();
|
|
expect(() => listPaymentCustomersQuerySchema.parse({ limit: 0 })).toThrow();
|
|
});
|
|
});
|