payment-kit 1.29.1 → 1.29.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/dev.ts +41 -2
- package/api/hono.d.ts +42 -0
- package/api/node-sqlite.d.ts +12 -0
- package/api/src/bootstrap.ts +36 -0
- package/api/src/crons/base.ts +3 -3
- package/api/src/crons/currency.ts +1 -1
- package/api/src/crons/index.ts +27 -24
- package/api/src/crons/metering-subscription-detection.ts +1 -1
- package/api/src/crons/overdue-detection.ts +2 -2
- package/api/src/crons/retry-pending-events.ts +6 -0
- package/api/src/index.ts +22 -161
- package/api/src/integrations/app-store/client.ts +3 -4
- package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
- package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
- package/api/src/integrations/arcblock/token.ts +21 -7
- package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
- package/api/src/integrations/google-play/handlers/voided.ts +2 -2
- package/api/src/integrations/google-play/verify.ts +3 -2
- package/api/src/integrations/iap-reconcile.ts +3 -5
- package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
- package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
- package/api/src/libs/archive/query.ts +19 -0
- package/api/src/libs/audit.ts +61 -4
- package/api/src/libs/auth.ts +99 -38
- package/api/src/libs/context.ts +78 -1
- package/api/src/libs/currency.ts +2 -2
- package/api/src/libs/dayjs.ts +8 -2
- package/api/src/libs/drivers/auth-storage.ts +118 -0
- package/api/src/libs/drivers/cron.ts +264 -0
- package/api/src/libs/drivers/db.ts +170 -0
- package/api/src/libs/drivers/identity.ts +81 -0
- package/api/src/libs/drivers/index.ts +40 -0
- package/api/src/libs/drivers/locks.ts +226 -0
- package/api/src/libs/drivers/migrate-runner.ts +70 -0
- package/api/src/libs/drivers/queue.ts +104 -0
- package/api/src/libs/drivers/secrets.ts +194 -0
- package/api/src/libs/env.ts +170 -54
- package/api/src/libs/exchange-rate/service.ts +7 -6
- package/api/src/libs/http-fetch-adapter.ts +50 -0
- package/api/src/libs/invoice.ts +1 -1
- package/api/src/libs/lock.ts +51 -47
- package/api/src/libs/logger.ts +48 -8
- package/api/src/libs/notification/index.ts +1 -1
- package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
- package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
- package/api/src/libs/overdraft-protection.ts +1 -1
- package/api/src/libs/payout.ts +1 -1
- package/api/src/libs/queue/index.ts +259 -52
- package/api/src/libs/queue/runtime.ts +175 -0
- package/api/src/libs/resource.ts +3 -3
- package/api/src/libs/secrets.ts +38 -0
- package/api/src/libs/session.ts +3 -2
- package/api/src/libs/subscription.ts +5 -5
- package/api/src/libs/tenant.ts +92 -0
- package/api/src/libs/url.ts +3 -3
- package/api/src/libs/util.ts +21 -13
- package/api/src/middlewares/hono/cdn.ts +63 -0
- package/api/src/middlewares/hono/context.ts +73 -0
- package/api/src/middlewares/hono/csrf.ts +72 -0
- package/api/src/middlewares/hono/fallback.ts +194 -0
- package/api/src/middlewares/hono/pipeline.ts +73 -0
- package/api/src/middlewares/hono/resource-mount.ts +42 -0
- package/api/src/middlewares/hono/resource.ts +63 -0
- package/api/src/middlewares/hono/security.ts +214 -0
- package/api/src/middlewares/hono/session.ts +114 -0
- package/api/src/middlewares/hono/xss.ts +61 -0
- package/api/src/queues/auto-recharge.ts +12 -10
- package/api/src/queues/checkout-session.ts +17 -12
- package/api/src/queues/credit-consume.ts +40 -36
- package/api/src/queues/credit-grant.ts +25 -18
- package/api/src/queues/credit-reconciliation.ts +7 -5
- package/api/src/queues/discount-status.ts +9 -6
- package/api/src/queues/event.ts +12 -4
- package/api/src/queues/exchange-rate-health.ts +49 -30
- package/api/src/queues/invoice.ts +18 -15
- package/api/src/queues/notification.ts +14 -7
- package/api/src/queues/payment.ts +41 -28
- package/api/src/queues/payout.ts +9 -5
- package/api/src/queues/refund.ts +18 -12
- package/api/src/queues/subscription.ts +83 -53
- package/api/src/queues/token-transfer.ts +15 -10
- package/api/src/queues/usage-record.ts +8 -5
- package/api/src/queues/vendors/commission.ts +7 -5
- package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
- package/api/src/queues/vendors/fulfillment.ts +4 -2
- package/api/src/queues/vendors/return-processor.ts +5 -3
- package/api/src/queues/vendors/return-scanner.ts +5 -4
- package/api/src/queues/vendors/status-check.ts +10 -7
- package/api/src/queues/webhook.ts +60 -32
- package/api/src/routes/connect/shared.ts +1 -2
- package/api/src/routes/connect/subscribe.ts +3 -3
- package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
- package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
- package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
- package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
- package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
- package/api/src/routes/hono/credit-tokens.ts +43 -0
- package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
- package/api/src/routes/{customers.ts → hono/customers.ts} +193 -223
- package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
- package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
- package/api/src/routes/{events.ts → hono/events.ts} +107 -71
- package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
- package/api/src/routes/hono/exchange-rates.ts +77 -0
- package/api/src/routes/hono/index.ts +115 -0
- package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
- package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
- package/api/src/routes/hono/integrations/stripe.ts +74 -0
- package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
- package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
- package/api/src/routes/hono/meters.ts +288 -0
- package/api/src/routes/hono/passports.ts +73 -0
- package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
- package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
- package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
- package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
- package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
- package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
- package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
- package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
- package/api/src/routes/{products.ts → hono/products.ts} +172 -159
- package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
- package/api/src/routes/hono/redirect.ts +24 -0
- package/api/src/routes/{refunds.ts → hono/refunds.ts} +96 -80
- package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
- package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
- package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
- package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
- package/api/src/routes/hono/tool.ts +69 -0
- package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
- package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
- package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
- package/api/src/routes/hono/webhook-endpoints.ts +126 -0
- package/api/src/service.ts +667 -0
- package/api/src/store/migrations/20230911-seeding.ts +2 -1
- package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
- package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
- package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
- package/api/src/store/models/auto-recharge-config.ts +22 -10
- package/api/src/store/models/checkout-session.ts +15 -14
- package/api/src/store/models/coupon.ts +29 -20
- package/api/src/store/models/credit-grant.ts +38 -29
- package/api/src/store/models/credit-transaction.ts +32 -21
- package/api/src/store/models/customer.ts +19 -17
- package/api/src/store/models/discount.ts +11 -2
- package/api/src/store/models/entitlement-grant.ts +21 -9
- package/api/src/store/models/entitlement-product.ts +21 -9
- package/api/src/store/models/entitlement.ts +19 -10
- package/api/src/store/models/event.ts +18 -9
- package/api/src/store/models/exchange-rate-provider.ts +17 -4
- package/api/src/store/models/invoice-item.ts +18 -9
- package/api/src/store/models/invoice.ts +16 -8
- package/api/src/store/models/meter-event.ts +27 -9
- package/api/src/store/models/meter.ts +31 -22
- package/api/src/store/models/payment-currency.ts +25 -8
- package/api/src/store/models/payment-intent.ts +15 -6
- package/api/src/store/models/payment-link.ts +15 -6
- package/api/src/store/models/payment-method.ts +38 -22
- package/api/src/store/models/payment-stat.ts +18 -9
- package/api/src/store/models/payout.ts +15 -6
- package/api/src/store/models/price-quote.ts +17 -8
- package/api/src/store/models/price.ts +24 -12
- package/api/src/store/models/pricing-table.ts +29 -20
- package/api/src/store/models/product-vendor.ts +20 -10
- package/api/src/store/models/product.ts +15 -6
- package/api/src/store/models/promotion-code.ts +14 -6
- package/api/src/store/models/refund.ts +15 -6
- package/api/src/store/models/revenue-snapshot.ts +21 -9
- package/api/src/store/models/setting.ts +18 -9
- package/api/src/store/models/setup-intent.ts +36 -27
- package/api/src/store/models/subscription-item.ts +21 -9
- package/api/src/store/models/subscription-schedule.ts +21 -9
- package/api/src/store/models/subscription.ts +21 -10
- package/api/src/store/models/tax-rate.ts +29 -21
- package/api/src/store/models/usage-record.ts +11 -2
- package/api/src/store/models/webhook-attempt.ts +18 -9
- package/api/src/store/models/webhook-endpoint.ts +18 -9
- package/api/src/store/scoped-core.ts +55 -0
- package/api/src/store/scoped.ts +247 -0
- package/api/src/store/sequelize.ts +66 -22
- package/api/src/store/sql-migrations.ts +20 -0
- package/api/src/store/tenant-backfill.ts +260 -0
- package/api/src/store/tenant-model.ts +124 -0
- package/api/src/store/tenant-tables.ts +50 -0
- package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
- package/api/tests/fixtures/bare-query-violation.ts +13 -0
- package/api/tests/fixtures/core-env-violation.ts +10 -0
- package/api/tests/fixtures/host-read-violation.ts +19 -0
- package/api/tests/fixtures/tenants.ts +4 -0
- package/api/tests/integrations/iap-tenant.spec.ts +284 -0
- package/api/tests/libs/archive-query.spec.ts +26 -0
- package/api/tests/libs/audit-tenant.spec.ts +153 -0
- package/api/tests/libs/context.spec.ts +204 -0
- package/api/tests/libs/core-config.spec.ts +115 -0
- package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
- package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
- package/api/tests/libs/lock-tenant.spec.ts +66 -0
- package/api/tests/libs/scoped.spec.ts +222 -0
- package/api/tests/libs/secrets-facade.spec.ts +52 -0
- package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
- package/api/tests/libs/tenant-middleware.spec.ts +42 -0
- package/api/tests/libs/tenant-scanner.spec.ts +120 -0
- package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
- package/api/tests/middlewares/hono/context.spec.ts +113 -0
- package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
- package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
- package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
- package/api/tests/middlewares/hono/security.spec.ts +181 -0
- package/api/tests/middlewares/hono/session.spec.ts +42 -0
- package/api/tests/middlewares/hono/xss.spec.ts +81 -0
- package/api/tests/models/tenant-backfill.spec.ts +287 -0
- package/api/tests/models/tenant-columns-model.spec.ts +46 -0
- package/api/tests/models/tenant-columns.spec.ts +161 -0
- package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
- package/api/tests/queues/credit-consume.spec.ts +8 -1
- package/api/tests/queues/event-tenant.spec.ts +236 -0
- package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
- package/api/tests/queues/queue-parity.spec.ts +249 -0
- package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
- package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
- package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
- package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
- package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
- package/api/tests/service/collapse.spec.ts +96 -0
- package/api/tests/store/tenant-crosscut.spec.ts +202 -0
- package/api/tests/store/tenant-model-spike.spec.ts +177 -0
- package/api/tests/store/tenant-model.spec.ts +162 -0
- package/api/tests/store/tenant-residual.spec.ts +196 -0
- package/api/third.d.ts +4 -0
- package/blocklet.yml +1 -1
- package/cloudflare/README.md +26 -6
- package/cloudflare/build.ts +28 -13
- package/cloudflare/did-connect-auth.ts +0 -217
- package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
- package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
- package/cloudflare/migrations/0008_schema_parity.sql +16 -0
- package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
- package/cloudflare/queue-runtime-mode.ts +13 -0
- package/cloudflare/run-build.js +10 -56
- package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
- package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
- package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
- package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
- package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
- package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
- package/cloudflare/shims/blocklet-sdk/util-csrf.ts +13 -0
- package/cloudflare/shims/blocklet-sdk/util-wallet.ts +8 -0
- package/cloudflare/shims/cron.ts +38 -158
- package/cloudflare/shims/events.ts +124 -0
- package/cloudflare/shims/fastq.ts +15 -1
- package/cloudflare/shims/nedb-storage.ts +16 -8
- package/cloudflare/shims/xss.ts +8 -0
- package/cloudflare/tenant-middleware.ts +36 -0
- package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
- package/cloudflare/tests/worker-handler-gate.spec.ts +44 -0
- package/cloudflare/worker.ts +204 -433
- package/cloudflare/wrangler.local-e2e.jsonc +26 -0
- package/jest.config.js +3 -1
- package/package.json +33 -38
- package/scripts/core-env-whitelist.json +1 -0
- package/scripts/e2e-12b-runtime.ts +149 -0
- package/scripts/e2e-core-config.ts +125 -0
- package/scripts/e2e-d1-tenancy.ts +116 -0
- package/scripts/e2e-d2-cron-queue.ts +139 -0
- package/scripts/e2e-d3-embedded-multi.ts +171 -0
- package/scripts/e2e-hono-s2.ts +125 -0
- package/scripts/e2e-hono-s3e.ts +135 -0
- package/scripts/e2e-hono-s4.ts +114 -0
- package/scripts/e2e-migration-contract.ts +100 -0
- package/scripts/e2e-s0.ts +61 -0
- package/scripts/e2e-s1.ts +107 -0
- package/scripts/e2e-s2.ts +178 -0
- package/scripts/e2e-s3.ts +110 -0
- package/scripts/e2e-s4.ts +191 -0
- package/scripts/e2e-s5.ts +139 -0
- package/scripts/e2e-s6.ts +127 -0
- package/scripts/e2e-tenant-model.ts +119 -0
- package/scripts/e2e-tenant-worker.ts +199 -0
- package/scripts/gen-sql-migrations.js +46 -0
- package/scripts/phase8-codemod.js +219 -0
- package/scripts/phase9a-env-getters-codemod.js +82 -0
- package/scripts/scan-core-env.js +109 -0
- package/scripts/scan-tenant-queries.js +235 -0
- package/scripts/schema-drift-guard.ts +210 -0
- package/scripts/tenant-scan-whitelist.json +1 -0
- package/src/env.d.ts +13 -1
- package/tsconfig.json +1 -1
- package/api/src/libs/did-space.ts +0 -235
- package/api/src/libs/middleware.ts +0 -50
- package/api/src/libs/security.ts +0 -192
- package/api/src/queues/space.ts +0 -662
- package/api/src/routes/credit-tokens.ts +0 -38
- package/api/src/routes/exchange-rates.ts +0 -87
- package/api/src/routes/index.ts +0 -142
- package/api/src/routes/integrations/stripe.ts +0 -61
- package/api/src/routes/meters.ts +0 -274
- package/api/src/routes/passports.ts +0 -68
- package/api/src/routes/redirect.ts +0 -20
- package/api/src/routes/tool.ts +0 -65
- package/api/src/routes/webhook-endpoints.ts +0 -126
- package/api/tests/routes/credit-grants.spec.ts +0 -1261
- package/cloudflare/shims/did-space-js.ts +0 -17
- package/cloudflare/shims/did-space.ts +0 -11
- package/cloudflare/shims/express-compat/index.ts +0 -80
- package/cloudflare/shims/express-compat/types.ts +0 -41
- package/cloudflare/shims/lock.ts +0 -115
- package/cloudflare/shims/queue.ts +0 -611
- package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
- package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { Sequelize } from 'sequelize';
|
|
5
|
+
import { SequelizeStorage, Umzug } from 'umzug';
|
|
6
|
+
|
|
7
|
+
import { withTenant } from '../../src/libs/context';
|
|
8
|
+
import { TENANT_CONTEXT_MISSING, TENANT_MISMATCH } from '../../src/libs/tenant';
|
|
9
|
+
import { TENANT_A, TENANT_B } from '../fixtures/tenants';
|
|
10
|
+
|
|
11
|
+
jest.mock('../../src/libs/logger', () => ({
|
|
12
|
+
__esModule: true,
|
|
13
|
+
default: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() },
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
// neutralize the queue engine: real createQueue scans the jobs table on
|
|
17
|
+
// import (crash-recovery), which races against this suite's temp DB lifecycle
|
|
18
|
+
jest.mock('../../src/libs/queue', () => ({
|
|
19
|
+
__esModule: true,
|
|
20
|
+
default: () => ({
|
|
21
|
+
push: jest.fn(),
|
|
22
|
+
pushAndWait: jest.fn(),
|
|
23
|
+
cancel: jest.fn(),
|
|
24
|
+
on: jest.fn(),
|
|
25
|
+
get: jest.fn().mockResolvedValue(null),
|
|
26
|
+
}),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
// the five delivery paths are exercised against mocks for transport pieces
|
|
30
|
+
const addWebhookJob = jest.fn().mockResolvedValue(true);
|
|
31
|
+
jest.mock('../../src/queues/webhook', () => {
|
|
32
|
+
const actual = jest.requireActual('../../src/queues/webhook');
|
|
33
|
+
return { ...actual, addWebhookJob: (...args: any[]) => addWebhookJob(...args) };
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const componentRequest = jest.fn().mockResolvedValue({ status: 200, data: { ok: true } });
|
|
37
|
+
jest.mock('@blocklet/sdk/lib/util/component-api', () => ({
|
|
38
|
+
__esModule: true,
|
|
39
|
+
default: { request: (...args: any[]) => componentRequest(...args) },
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
const STORE_DIR = path.join(__dirname, '../../src/store');
|
|
43
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'event-tenant-'));
|
|
44
|
+
const sequelize = new Sequelize({ dialect: 'sqlite', storage: path.join(dir, 'test.db'), logging: false });
|
|
45
|
+
const umzug = new Umzug({
|
|
46
|
+
migrations: {
|
|
47
|
+
glob: ['migrations/*.ts', { cwd: STORE_DIR }],
|
|
48
|
+
resolve: ({ name, path: p, context }) => {
|
|
49
|
+
// eslint-disable-next-line import/no-dynamic-require, global-require
|
|
50
|
+
const migration = require(p!);
|
|
51
|
+
return {
|
|
52
|
+
name: name.replace(/\.ts$/, '.js'),
|
|
53
|
+
up: () => migration.up({ context }),
|
|
54
|
+
down: () => migration.down({ context }),
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
context: sequelize.getQueryInterface(),
|
|
59
|
+
storage: new SequelizeStorage({ sequelize }),
|
|
60
|
+
logger: undefined,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
let models: any;
|
|
64
|
+
let handleEvent: any;
|
|
65
|
+
let handleWebhook: any;
|
|
66
|
+
let assertEventTenantAccessible: any;
|
|
67
|
+
let logger: any;
|
|
68
|
+
|
|
69
|
+
beforeAll(async () => {
|
|
70
|
+
await umzug.up();
|
|
71
|
+
// eslint-disable-next-line global-require
|
|
72
|
+
models = require('../../src/store/models');
|
|
73
|
+
models.initialize(sequelize);
|
|
74
|
+
// eslint-disable-next-line global-require
|
|
75
|
+
({ handleEvent } = require('../../src/queues/event'));
|
|
76
|
+
// eslint-disable-next-line global-require
|
|
77
|
+
({ handleWebhook } = jest.requireActual('../../src/queues/webhook'));
|
|
78
|
+
// eslint-disable-next-line global-require
|
|
79
|
+
({ assertEventTenantAccessible } = require('../../src/routes/hono/events'));
|
|
80
|
+
// eslint-disable-next-line global-require
|
|
81
|
+
logger = require('../../src/libs/logger').default;
|
|
82
|
+
}, 120000);
|
|
83
|
+
|
|
84
|
+
afterAll(async () => {
|
|
85
|
+
await sequelize.close();
|
|
86
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const seedEvent = (tenant: string, type = 'customer.updated') =>
|
|
90
|
+
withTenant(tenant, () =>
|
|
91
|
+
models.Event.create({
|
|
92
|
+
type,
|
|
93
|
+
instance_did: tenant,
|
|
94
|
+
api_version: 'test',
|
|
95
|
+
livemode: false,
|
|
96
|
+
object_id: 'obj_1',
|
|
97
|
+
object_type: 'customer',
|
|
98
|
+
data: { object: { id: 'obj_1' } },
|
|
99
|
+
request: { id: '', idempotency_key: '', requested_by: 'test' },
|
|
100
|
+
metadata: {},
|
|
101
|
+
pending_webhooks: 99,
|
|
102
|
+
})
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const seedEndpoint = (tenant: string, url: string) =>
|
|
106
|
+
withTenant(tenant, () =>
|
|
107
|
+
models.WebhookEndpoint.create({
|
|
108
|
+
instance_did: tenant,
|
|
109
|
+
livemode: false,
|
|
110
|
+
url,
|
|
111
|
+
description: 'test',
|
|
112
|
+
status: 'enabled',
|
|
113
|
+
enabled_events: ['customer.updated'],
|
|
114
|
+
secret: 'whsec_test',
|
|
115
|
+
api_version: 'test',
|
|
116
|
+
})
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
beforeEach(async () => {
|
|
120
|
+
jest.clearAllMocks();
|
|
121
|
+
await sequelize.query('DELETE FROM events');
|
|
122
|
+
await sequelize.query('DELETE FROM webhook_endpoints');
|
|
123
|
+
await sequelize.query('DELETE FROM webhook_attempts');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('event delivery tenant isolation (phase 4)', () => {
|
|
127
|
+
describe('path 1 fanout (queues/event.ts): only same-tenant endpoints scheduled', () => {
|
|
128
|
+
it('A event fans out to A endpoint only, B endpoint untouched', async () => {
|
|
129
|
+
const event = await seedEvent(TENANT_A);
|
|
130
|
+
const endpointA = await seedEndpoint(TENANT_A, 'http://a.example.com/hook');
|
|
131
|
+
await seedEndpoint(TENANT_B, 'http://b.example.com/hook');
|
|
132
|
+
|
|
133
|
+
await handleEvent({ eventId: event.id });
|
|
134
|
+
|
|
135
|
+
expect(addWebhookJob).toHaveBeenCalledTimes(1);
|
|
136
|
+
expect(addWebhookJob).toHaveBeenCalledWith(event.id, endpointA.id, expect.anything());
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('path 2 delivery handler (queues/webhook.ts): tenant invariant', () => {
|
|
141
|
+
it('forged job pairing A event with B endpoint is refused without an attempt row', async () => {
|
|
142
|
+
const event = await seedEvent(TENANT_A);
|
|
143
|
+
const endpointB = await seedEndpoint(TENANT_B, 'http://b.example.com/hook');
|
|
144
|
+
|
|
145
|
+
await handleWebhook({ eventId: event.id, webhookId: endpointB.id });
|
|
146
|
+
|
|
147
|
+
expect(componentRequest).not.toHaveBeenCalled();
|
|
148
|
+
const [attempts] = await sequelize.query('SELECT COUNT(*) AS n FROM webhook_attempts');
|
|
149
|
+
expect((attempts as any[])[0].n).toBe(0);
|
|
150
|
+
expect(logger.error).toHaveBeenCalledWith(
|
|
151
|
+
expect.stringContaining('tenant mismatch'),
|
|
152
|
+
expect.objectContaining({ code: TENANT_MISMATCH })
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('same-tenant delivery succeeds and the attempt row carries the tenant', async () => {
|
|
157
|
+
const event = await seedEvent(TENANT_A);
|
|
158
|
+
const endpointA = await seedEndpoint(TENANT_A, 'http://a.example.com/hook');
|
|
159
|
+
|
|
160
|
+
await handleWebhook({ eventId: event.id, webhookId: endpointA.id });
|
|
161
|
+
|
|
162
|
+
expect(componentRequest).toHaveBeenCalledTimes(1);
|
|
163
|
+
const [attempts] = await sequelize.query('SELECT status, instance_did FROM webhook_attempts');
|
|
164
|
+
expect(attempts).toEqual([{ status: 'succeeded', instance_did: TENANT_A }]);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('paths 3+4 manual retry routes: caller tenant guard', () => {
|
|
169
|
+
it('A caller cannot retry a B event (TENANT_MISMATCH -> 4xx mapping)', async () => {
|
|
170
|
+
await withTenant(TENANT_A, async () => {
|
|
171
|
+
expect(() => assertEventTenantAccessible({ instance_did: TENANT_B })).toThrow(
|
|
172
|
+
expect.objectContaining({ code: TENANT_MISMATCH })
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('multi mode without caller context fails closed', () => {
|
|
178
|
+
process.env.PAYMENT_TENANT_MODE = 'multi';
|
|
179
|
+
try {
|
|
180
|
+
expect(() => assertEventTenantAccessible({ instance_did: TENANT_B })).toThrow(
|
|
181
|
+
expect.objectContaining({ code: TENANT_CONTEXT_MISSING })
|
|
182
|
+
);
|
|
183
|
+
} finally {
|
|
184
|
+
delete process.env.PAYMENT_TENANT_MODE;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('same-tenant caller passes the guard', async () => {
|
|
189
|
+
await withTenant(TENANT_B, async () => {
|
|
190
|
+
expect(() => assertEventTenantAccessible({ instance_did: TENANT_B })).not.toThrow();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('retry fanout never schedules the other tenant endpoint (paths 3+4 data leak)', async () => {
|
|
195
|
+
// same shape the retry routes use after their tenant guard: endpoint
|
|
196
|
+
// query scoped by caller tenant -> B endpoint invisible to A
|
|
197
|
+
await seedEndpoint(TENANT_A, 'http://a.example.com/hook');
|
|
198
|
+
await seedEndpoint(TENANT_B, 'http://b.example.com/hook');
|
|
199
|
+
const visible = await withTenant(TENANT_A, async () =>
|
|
200
|
+
models.WebhookEndpoint.findAll({
|
|
201
|
+
where: { status: 'enabled', livemode: false, instance_did: TENANT_A },
|
|
202
|
+
})
|
|
203
|
+
);
|
|
204
|
+
expect(visible).toHaveLength(1);
|
|
205
|
+
expect(visible[0].url).toBe('http://a.example.com/hook');
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('path 5 pending scan (crons/retry-pending-events.ts)', () => {
|
|
210
|
+
it('re-enqueued events still fan out tenant-filtered (transitively via handleEvent)', async () => {
|
|
211
|
+
// the cron only re-enqueues IDs; prove the downstream filter holds for a
|
|
212
|
+
// B event when both tenants have endpoints
|
|
213
|
+
const event = await seedEvent(TENANT_B);
|
|
214
|
+
await seedEndpoint(TENANT_A, 'http://a.example.com/hook');
|
|
215
|
+
const endpointB = await seedEndpoint(TENANT_B, 'http://b.example.com/hook');
|
|
216
|
+
|
|
217
|
+
await handleEvent({ eventId: event.id });
|
|
218
|
+
|
|
219
|
+
expect(addWebhookJob).toHaveBeenCalledTimes(1);
|
|
220
|
+
expect(addWebhookJob).toHaveBeenCalledWith(event.id, endpointB.id, expect.anything());
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('data damage: retry keeps the original tenant', () => {
|
|
225
|
+
it('failed delivery writes a failed attempt under the event tenant', async () => {
|
|
226
|
+
componentRequest.mockRejectedValueOnce(Object.assign(new Error('boom'), { response: { status: 500 } }));
|
|
227
|
+
const event = await seedEvent(TENANT_A);
|
|
228
|
+
const endpointA = await seedEndpoint(TENANT_A, 'http://a.example.com/hook');
|
|
229
|
+
|
|
230
|
+
await handleWebhook({ eventId: event.id, webhookId: endpointA.id });
|
|
231
|
+
|
|
232
|
+
const [attempts] = await sequelize.query('SELECT status, instance_did FROM webhook_attempts');
|
|
233
|
+
expect(attempts).toEqual([{ status: 'failed', instance_did: TENANT_A }]);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// D6 — the exchange-rate-health schedule carries its tenant in the job PAYLOAD,
|
|
2
|
+
// so the re-schedule (on 'finished', which fires OUTSIDE any withTenant scope)
|
|
3
|
+
// stays under the correct tenant without relying on the ALS context. In multi
|
|
4
|
+
// mode a tenant-less push would throw TENANT_CONTEXT_MISSING.
|
|
5
|
+
|
|
6
|
+
jest.mock('../../src/libs/logger', () => ({
|
|
7
|
+
__esModule: true,
|
|
8
|
+
default: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() },
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
const pushed: any[] = [];
|
|
12
|
+
let finishedListener: ((data: any) => void) | undefined;
|
|
13
|
+
|
|
14
|
+
jest.mock('../../src/libs/queue', () => ({
|
|
15
|
+
__esModule: true,
|
|
16
|
+
default: () => ({
|
|
17
|
+
push: (p: any) => {
|
|
18
|
+
pushed.push(p);
|
|
19
|
+
return { on: jest.fn() };
|
|
20
|
+
},
|
|
21
|
+
on: (ev: string, cb: any) => {
|
|
22
|
+
if (ev === 'finished') finishedListener = cb;
|
|
23
|
+
},
|
|
24
|
+
pushAndWait: jest.fn(),
|
|
25
|
+
cancel: jest.fn(),
|
|
26
|
+
get: jest.fn(),
|
|
27
|
+
store: { addJob: jest.fn(), getScheduledJobs: jest.fn().mockResolvedValue([]) },
|
|
28
|
+
stop: jest.fn(),
|
|
29
|
+
}),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
import { scheduleHealthChecks } from '../../src/queues/exchange-rate-health';
|
|
33
|
+
|
|
34
|
+
describe('D6 — exchange-rate-health carries the tenant in the job payload', () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
pushed.length = 0;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('the initial schedule pushes a job tagged with instance_did', () => {
|
|
40
|
+
scheduleHealthChecks('did:abt:zHEALTHA');
|
|
41
|
+
expect(pushed.length).toBe(1);
|
|
42
|
+
expect(pushed[0].job.instance_did).toBe('did:abt:zHEALTHA');
|
|
43
|
+
expect(pushed[0].persist).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('the re-schedule preserves the FINISHED job’s tenant (no ALS reliance)', () => {
|
|
47
|
+
scheduleHealthChecks('did:abt:zHEALTHA');
|
|
48
|
+
expect(finishedListener).toBeDefined();
|
|
49
|
+
pushed.length = 0;
|
|
50
|
+
// simulate a finished job that belonged to tenant B — the next schedule must
|
|
51
|
+
// be for B, taken from the job payload, NOT from any ambient context.
|
|
52
|
+
finishedListener!({ job: { type: 'health_check', timestamp: 1, instance_did: 'did:abt:zHEALTHB' } });
|
|
53
|
+
expect(pushed.length).toBe(1);
|
|
54
|
+
expect(pushed[0].job.instance_did).toBe('did:abt:zHEALTHB');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('single mode (no instanceDid) pushes without a forced tenant (default-tenant fallback applies)', () => {
|
|
58
|
+
scheduleHealthChecks(undefined);
|
|
59
|
+
expect(pushed.length).toBe(1);
|
|
60
|
+
expect(pushed[0].job.instance_did).toBeUndefined(); // injectJobTenant uses the default tenant in single
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// Phase 9 (W2-1b): queue engine + executor parity (W2 判据 4).
|
|
2
|
+
//
|
|
3
|
+
// The CF worker runs the SAME queue engine as Blocklet Server
|
|
4
|
+
// (api/src/libs/queue) — only the fastq EXECUTOR primitive is swapped (real
|
|
5
|
+
// fastq on Node, cloudflare/shims/fastq in the worker). This spec proves:
|
|
6
|
+
// Part A — the engine's delay/retry/cancel/pushAndWait semantics (embedded).
|
|
7
|
+
// Part B — the fastq shim is a faithful drop-in for real fastq on the exact
|
|
8
|
+
// operations the engine uses, so the worker behaves identically.
|
|
9
|
+
// Together that is the embedded↔worker queue parity the acceptance gate wants.
|
|
10
|
+
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import os from 'os';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { Sequelize } from 'sequelize';
|
|
15
|
+
import { SequelizeStorage, Umzug } from 'umzug';
|
|
16
|
+
|
|
17
|
+
import { withTenant, getInstanceDid } from '../../src/libs/context';
|
|
18
|
+
|
|
19
|
+
/* eslint-disable global-require, import/no-dynamic-require, require-await, no-promise-executor-return */
|
|
20
|
+
|
|
21
|
+
// the retry/settle cases do real per-attempt DB ops on a shared sqlite file;
|
|
22
|
+
// under full-suite parallel load they can exceed jest's 5s default, so give the
|
|
23
|
+
// suite a generous timeout (these are inherently I/O-bound, not hung). Phase 12b
|
|
24
|
+
// added queue-runtime-surface.spec.ts which contends for the same jest workers,
|
|
25
|
+
// so the margin is bumped to keep the settle cases from a transient timeout.
|
|
26
|
+
jest.setTimeout(60000);
|
|
27
|
+
|
|
28
|
+
jest.mock('../../src/libs/logger', () => ({
|
|
29
|
+
__esModule: true,
|
|
30
|
+
default: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() },
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
const STORE_DIR = path.join(__dirname, '../../src/store');
|
|
34
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'queue-parity-'));
|
|
35
|
+
const sequelize = new Sequelize({ dialect: 'sqlite', storage: path.join(dir, 'test.db'), logging: false });
|
|
36
|
+
const umzug = new Umzug({
|
|
37
|
+
migrations: {
|
|
38
|
+
glob: ['migrations/*.ts', { cwd: STORE_DIR }],
|
|
39
|
+
resolve: ({ name, path: p, context }) => {
|
|
40
|
+
const migration = require(p!);
|
|
41
|
+
return {
|
|
42
|
+
name: name.replace(/\.ts$/, '.js'),
|
|
43
|
+
up: () => migration.up({ context }),
|
|
44
|
+
down: () => migration.down({ context }),
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
context: sequelize.getQueryInterface(),
|
|
49
|
+
storage: new SequelizeStorage({ sequelize }),
|
|
50
|
+
logger: undefined,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
let createQueue: any;
|
|
54
|
+
|
|
55
|
+
beforeAll(async () => {
|
|
56
|
+
await umzug.up();
|
|
57
|
+
const models = require('../../src/store/models');
|
|
58
|
+
models.initialize(sequelize);
|
|
59
|
+
createQueue = require('../../src/libs/queue').default;
|
|
60
|
+
}, 120000);
|
|
61
|
+
|
|
62
|
+
afterAll(async () => {
|
|
63
|
+
await sequelize.close();
|
|
64
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const settle = (emitter: any): Promise<{ event: string; data: any }> =>
|
|
68
|
+
new Promise((resolve) => {
|
|
69
|
+
['finished', 'failed', 'cancelled'].forEach((e) => emitter.on(e, (data: any) => resolve({ event: e, data })));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('Part A — Node queue engine contract semantics', () => {
|
|
73
|
+
beforeEach(async () => {
|
|
74
|
+
await sequelize.query('DELETE FROM jobs');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('immediate job runs and the jobs row is cleared on finish', async () => {
|
|
78
|
+
const seen: any[] = [];
|
|
79
|
+
const q = createQueue({
|
|
80
|
+
name: `pa-imm-${Date.now()}`,
|
|
81
|
+
onJob: async (job: any) => {
|
|
82
|
+
seen.push(job.v);
|
|
83
|
+
return job.v * 2;
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
const ev = q.push({ job: { v: 21 } });
|
|
87
|
+
const { event, data } = await settle(ev);
|
|
88
|
+
expect(event).toBe('finished');
|
|
89
|
+
expect(data.result).toBe(42);
|
|
90
|
+
expect(seen).toEqual([21]);
|
|
91
|
+
const row = await q.get(ev.id);
|
|
92
|
+
expect(row).toBeNull(); // cleared
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('pushAndWait resolves with the handler result', async () => {
|
|
96
|
+
const q = createQueue({ name: `pa-paw-${Date.now()}`, onJob: async (job: any) => `ok:${job.v}` });
|
|
97
|
+
const res: any = await q.pushAndWait({ job: { v: 7 } });
|
|
98
|
+
expect(res.result).toBe('ok:7');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('retries up to maxRetries then fails (no infinite retry)', async () => {
|
|
102
|
+
let attempts = 0;
|
|
103
|
+
const retries: number[] = [];
|
|
104
|
+
const q = createQueue({
|
|
105
|
+
name: `pa-retry-${Date.now()}`,
|
|
106
|
+
onJob: async () => {
|
|
107
|
+
attempts += 1;
|
|
108
|
+
throw new Error('always');
|
|
109
|
+
},
|
|
110
|
+
options: { maxRetries: 3, retryDelay: 1 },
|
|
111
|
+
});
|
|
112
|
+
q.on('retry', () => retries.push(1));
|
|
113
|
+
const ev = q.push({ job: { v: 1 } });
|
|
114
|
+
const { event } = await settle(ev);
|
|
115
|
+
expect(event).toBe('failed');
|
|
116
|
+
expect(attempts).toBe(3); // initial + 2 retries (retry_count starts at 1, fails at >= maxRetries)
|
|
117
|
+
expect(retries.length).toBeGreaterThanOrEqual(1);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('nonRetryable error fails immediately (single attempt)', async () => {
|
|
121
|
+
let attempts = 0;
|
|
122
|
+
const q = createQueue({
|
|
123
|
+
name: `pa-nonretry-${Date.now()}`,
|
|
124
|
+
onJob: async () => {
|
|
125
|
+
attempts += 1;
|
|
126
|
+
const err: any = new Error('forged');
|
|
127
|
+
err.nonRetryable = true;
|
|
128
|
+
throw err;
|
|
129
|
+
},
|
|
130
|
+
options: { maxRetries: 5, retryDelay: 1 },
|
|
131
|
+
});
|
|
132
|
+
const ev = q.push({ job: { v: 1 } });
|
|
133
|
+
const { event } = await settle(ev);
|
|
134
|
+
expect(event).toBe('failed');
|
|
135
|
+
expect(attempts).toBe(1);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('cancel marks the row and excludes it from recovery (the skip mechanism)', async () => {
|
|
139
|
+
const q = createQueue({ name: `pa-cancel-${Date.now()}`, onJob: async () => undefined });
|
|
140
|
+
await q.store.addJob('cj-1', { v: 1, instance_did: 'did:abt:zCANCEL' }, {});
|
|
141
|
+
// before cancel: recovery would pick the row up
|
|
142
|
+
expect((await q.store.getJobs()).some((j: any) => j.id === 'cj-1')).toBe(true);
|
|
143
|
+
await q.cancel('cj-1');
|
|
144
|
+
// after cancel: execution-time guard sees it, recovery query excludes it
|
|
145
|
+
expect(await q.store.isCancelled('cj-1')).toBe(true);
|
|
146
|
+
expect((await q.store.getJobs()).some((j: any) => j.id === 'cj-1')).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Bad input
|
|
150
|
+
it('rejects an empty job', () => {
|
|
151
|
+
const q = createQueue({ name: `pa-empty-${Date.now()}`, onJob: async () => undefined });
|
|
152
|
+
expect(() => q.push({ job: undefined as any })).toThrow(/Can not queue empty job/);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Data damage — same job id does not execute twice (duplicate guard)
|
|
156
|
+
it('a duplicate job id executes only once', async () => {
|
|
157
|
+
let runs = 0;
|
|
158
|
+
const q = createQueue({
|
|
159
|
+
name: `pa-dup-${Date.now()}`,
|
|
160
|
+
onJob: async () => {
|
|
161
|
+
runs += 1;
|
|
162
|
+
return runs;
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
q.push({ job: { v: 1 }, id: 'dup-1' });
|
|
166
|
+
q.push({ job: { v: 1 }, id: 'dup-1' }); // duplicate id — store rejects, no second execution
|
|
167
|
+
// Either push can win the concurrent addJob UNIQUE race; the LOSER's
|
|
168
|
+
// jobEvents never settles (its addJob rejects before queueJob), so awaiting
|
|
169
|
+
// one specific push is racy and starves under parallel load. Wait for the
|
|
170
|
+
// single execution to actually land instead — that is what we assert.
|
|
171
|
+
const deadline = Date.now() + 5000;
|
|
172
|
+
while (runs < 1 && Date.now() < deadline) {
|
|
173
|
+
// eslint-disable-next-line no-await-in-loop, no-promise-executor-return
|
|
174
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
175
|
+
}
|
|
176
|
+
await new Promise((r) => setTimeout(r, 60));
|
|
177
|
+
expect(runs).toBe(1);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Data leak — the handler runs under the PAYLOAD tenant, never another's
|
|
181
|
+
it('handler executes under the payload tenant (no cross-tenant leak)', async () => {
|
|
182
|
+
const seen: Record<string, string> = {};
|
|
183
|
+
const q = createQueue({
|
|
184
|
+
name: `pa-tenant-${Date.now()}`,
|
|
185
|
+
onJob: async (job: any) => {
|
|
186
|
+
seen[job.tag] = getInstanceDid();
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
// push under tenant A's context — payload is stamped with A
|
|
190
|
+
const evA = await withTenant('did:abt:zTENANTA', async () => q.push({ job: { tag: 'A' } }));
|
|
191
|
+
const evB = await withTenant('did:abt:zTENANTB', async () => q.push({ job: { tag: 'B' } }));
|
|
192
|
+
await Promise.all([settle(evA), settle(evB)]);
|
|
193
|
+
expect(seen.A).toBe('did:abt:zTENANTA');
|
|
194
|
+
expect(seen.B).toBe('did:abt:zTENANTB');
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('Part B — fastq shim is a faithful drop-in for real fastq', () => {
|
|
199
|
+
// run the exact operations the engine uses against both executors
|
|
200
|
+
const realFastq = require('fastq');
|
|
201
|
+
const shimFastq = require('../../../cloudflare/shims/fastq').default;
|
|
202
|
+
|
|
203
|
+
const scenario = (fastqImpl: any) =>
|
|
204
|
+
new Promise<any>((resolve) => {
|
|
205
|
+
const order: string[] = [];
|
|
206
|
+
const results: any[] = [];
|
|
207
|
+
const q = fastqImpl(async (data: any, cb: Function) => {
|
|
208
|
+
order.push(data.id);
|
|
209
|
+
if (data.fail) {
|
|
210
|
+
cb(new Error(`fail:${data.id}`));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
cb(null, `done:${data.id}`);
|
|
214
|
+
}, 1);
|
|
215
|
+
let pending = 3;
|
|
216
|
+
const done = (tag: string) => (err: any, res: any) => {
|
|
217
|
+
results.push({ tag, err: err?.message ?? null, res: res ?? null });
|
|
218
|
+
pending -= 1;
|
|
219
|
+
if (pending === 0) resolve({ order, results });
|
|
220
|
+
};
|
|
221
|
+
q.push({ id: 'a' }, done('a'));
|
|
222
|
+
q.push({ id: 'b', fail: true }, done('b'));
|
|
223
|
+
q.unshift({ id: 'c' }, done('c')); // unshift jumps the queue (retry path)
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('produces identical execution order and results on both executors', async () => {
|
|
227
|
+
const real = await scenario(realFastq);
|
|
228
|
+
const shim = await scenario(shimFastq);
|
|
229
|
+
// same success/error callback shape per job
|
|
230
|
+
const norm = (r: any) => r.results.sort((x: any, y: any) => x.tag.localeCompare(y.tag));
|
|
231
|
+
expect(norm(shim)).toEqual(norm(real));
|
|
232
|
+
// job 'b' errored on both, 'a'/'c' succeeded on both
|
|
233
|
+
const byTag = (r: any) => Object.fromEntries(r.results.map((x: any) => [x.tag, x.err ? 'err' : 'ok']));
|
|
234
|
+
expect(byTag(shim)).toEqual(byTag(real));
|
|
235
|
+
expect(byTag(real)).toEqual({ a: 'ok', b: 'err', c: 'ok' });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('the shim accepts the 2-arg fastq(worker, concurrency) form the engine uses', async () => {
|
|
239
|
+
// regression for the pre-existing "worker is not a function" bug
|
|
240
|
+
const ran: any[] = [];
|
|
241
|
+
const q = shimFastq(async (data: any, cb: Function) => {
|
|
242
|
+
ran.push(data.id);
|
|
243
|
+
cb(null, 'ok');
|
|
244
|
+
}, 1);
|
|
245
|
+
const res = await new Promise((r) => q.push({ id: 'x' }, (_e: any, v: any) => r(v)));
|
|
246
|
+
expect(ran).toEqual(['x']);
|
|
247
|
+
expect(res).toBe('ok');
|
|
248
|
+
});
|
|
249
|
+
});
|