Files
wehub-resource-sync 3a28426bf4
Lint and Format Check / lint-and-format (push) Failing after 0s
Check Migrations / Check for duplicate migration numbers (push) Failing after 1s
CI Pre-merge Check / CI Pre-merge Check (push) Failing after 2m17s
chore: import upstream snapshot with attribution
2026-07-13 12:23:40 +08:00

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