payment-kit 1.29.0 → 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/docs/2026-06-10-bundle-size-analysis.md +288 -0
- 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 +31 -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/node-fetch.ts +35 -0
- 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,287 @@
|
|
|
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 { runTenantBackfill } from '../../src/store/tenant-backfill';
|
|
8
|
+
import { TENANT_TABLES } from '../../src/store/tenant-tables';
|
|
9
|
+
import { getDefaultInstanceDid } from '../../src/libs/tenant';
|
|
10
|
+
import { TENANT_A, TENANT_B } from '../fixtures/tenants';
|
|
11
|
+
|
|
12
|
+
const STORE_DIR = path.join(__dirname, '../../src/store');
|
|
13
|
+
|
|
14
|
+
function createHarness(storagePath: string) {
|
|
15
|
+
const sequelize = new Sequelize({ dialect: 'sqlite', storage: storagePath, logging: false });
|
|
16
|
+
const umzug = new Umzug({
|
|
17
|
+
migrations: {
|
|
18
|
+
glob: ['migrations/*.ts', { cwd: STORE_DIR }],
|
|
19
|
+
resolve: ({ name, path: migrationPath, context }) => {
|
|
20
|
+
// eslint-disable-next-line import/no-dynamic-require, global-require
|
|
21
|
+
const migration = require(migrationPath!);
|
|
22
|
+
return {
|
|
23
|
+
name: name.replace(/\.ts$/, '.js'),
|
|
24
|
+
up: () => migration.up({ context }),
|
|
25
|
+
down: () => migration.down({ context }),
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
context: sequelize.getQueryInterface(),
|
|
30
|
+
storage: new SequelizeStorage({ sequelize }),
|
|
31
|
+
logger: undefined,
|
|
32
|
+
});
|
|
33
|
+
return { sequelize, umzug };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const now = () => new Date().toISOString();
|
|
37
|
+
|
|
38
|
+
async function countNulls(sequelize: Sequelize, table: string): Promise<number> {
|
|
39
|
+
const [rows] = await sequelize.query(`SELECT COUNT(*) AS n FROM ${table} WHERE instance_did IS NULL`);
|
|
40
|
+
return (rows as any[])[0].n;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('tenant backfill migration (phase 2)', () => {
|
|
44
|
+
let dir: string;
|
|
45
|
+
let harness: ReturnType<typeof createHarness>;
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tenant-backfill-'));
|
|
49
|
+
harness = createHarness(path.join(dir, 'test.db'));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(async () => {
|
|
53
|
+
await harness.sequelize.close();
|
|
54
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
async function migrateToJustBeforeBackfill() {
|
|
58
|
+
await harness.umzug.up();
|
|
59
|
+
await harness.umzug.down(); // revert only the backfill migration (latest)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe('happy path', () => {
|
|
63
|
+
it('backfills every tenant table to zero NULLs with the app DID', async () => {
|
|
64
|
+
await migrateToJustBeforeBackfill();
|
|
65
|
+
const qi = harness.sequelize.getQueryInterface();
|
|
66
|
+
await qi.bulkInsert('customers', [
|
|
67
|
+
{ id: 'cus_bf_1', livemode: 0, did: 'z-did-bf-1', delinquent: 0, created_at: now(), updated_at: now() },
|
|
68
|
+
]);
|
|
69
|
+
await qi.bulkInsert('products', [
|
|
70
|
+
{ id: 'prod_bf_1', livemode: 0, active: 1, name: 'bf', type: 'service', created_at: now(), updated_at: now() },
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
await harness.umzug.up(); // runs the backfill migration
|
|
74
|
+
|
|
75
|
+
for (const table of TENANT_TABLES) {
|
|
76
|
+
// eslint-disable-next-line no-await-in-loop
|
|
77
|
+
expect({ table, nulls: await countNulls(harness.sequelize, table) }).toEqual({ table, nulls: 0 });
|
|
78
|
+
}
|
|
79
|
+
const [rows] = await harness.sequelize.query("SELECT instance_did FROM customers WHERE id = 'cus_bf_1'");
|
|
80
|
+
expect((rows as any[])[0].instance_did).toBe(getDefaultInstanceDid());
|
|
81
|
+
}, 120000);
|
|
82
|
+
|
|
83
|
+
it('allows (A, key) and (B, key) to coexist after the unique revision', async () => {
|
|
84
|
+
await harness.umzug.up();
|
|
85
|
+
const qi = harness.sequelize.getQueryInterface();
|
|
86
|
+
await qi.bulkInsert('meters', [
|
|
87
|
+
{
|
|
88
|
+
id: 'mtr_a',
|
|
89
|
+
livemode: 0,
|
|
90
|
+
event_name: 'shared.event',
|
|
91
|
+
name: 'a',
|
|
92
|
+
unit: 'unit',
|
|
93
|
+
created_via: 'api',
|
|
94
|
+
instance_did: TENANT_A,
|
|
95
|
+
created_at: now(),
|
|
96
|
+
updated_at: now(),
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'mtr_b',
|
|
100
|
+
livemode: 0,
|
|
101
|
+
event_name: 'shared.event',
|
|
102
|
+
name: 'b',
|
|
103
|
+
unit: 'unit',
|
|
104
|
+
created_via: 'api',
|
|
105
|
+
instance_did: TENANT_B,
|
|
106
|
+
created_at: now(),
|
|
107
|
+
updated_at: now(),
|
|
108
|
+
},
|
|
109
|
+
]);
|
|
110
|
+
const [rows] = await harness.sequelize.query(
|
|
111
|
+
"SELECT COUNT(*) AS n FROM meters WHERE event_name = 'shared.event'"
|
|
112
|
+
);
|
|
113
|
+
expect((rows as any[])[0].n).toBe(2);
|
|
114
|
+
}, 120000);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('bad input', () => {
|
|
118
|
+
it('does not overwrite rows that already carry a tenant', async () => {
|
|
119
|
+
await migrateToJustBeforeBackfill();
|
|
120
|
+
const qi = harness.sequelize.getQueryInterface();
|
|
121
|
+
await qi.bulkInsert('customers', [
|
|
122
|
+
{
|
|
123
|
+
id: 'cus_keep',
|
|
124
|
+
livemode: 0,
|
|
125
|
+
did: 'z-did-keep',
|
|
126
|
+
delinquent: 0,
|
|
127
|
+
instance_did: TENANT_B,
|
|
128
|
+
created_at: now(),
|
|
129
|
+
updated_at: now(),
|
|
130
|
+
},
|
|
131
|
+
]);
|
|
132
|
+
await harness.umzug.up();
|
|
133
|
+
const [rows] = await harness.sequelize.query("SELECT instance_did FROM customers WHERE id = 'cus_keep'");
|
|
134
|
+
expect((rows as any[])[0].instance_did).toBe(TENANT_B);
|
|
135
|
+
}, 120000);
|
|
136
|
+
|
|
137
|
+
it('is safe on empty tables and re-runnable (idempotent)', async () => {
|
|
138
|
+
await harness.umzug.up();
|
|
139
|
+
const first = await runTenantBackfill(harness.sequelize);
|
|
140
|
+
const second = await runTenantBackfill(harness.sequelize);
|
|
141
|
+
expect(Object.values(second.backfilled).every((n) => n === 0)).toBe(true);
|
|
142
|
+
expect(first.instanceDid).toBe(getDefaultInstanceDid());
|
|
143
|
+
}, 120000);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('security', () => {
|
|
147
|
+
it('derives the backfill value only from the configured app DID getter', async () => {
|
|
148
|
+
await harness.umzug.up();
|
|
149
|
+
const result = await runTenantBackfill(harness.sequelize);
|
|
150
|
+
expect(result.instanceDid).toBe(getDefaultInstanceDid());
|
|
151
|
+
const source = fs.readFileSync(path.join(STORE_DIR, 'tenant-backfill.ts'), 'utf8');
|
|
152
|
+
// no request- or row-content-derived tenant paths
|
|
153
|
+
expect(source).not.toMatch(/req\.|headers|x-forwarded/i);
|
|
154
|
+
expect(source).toContain('getDefaultInstanceDid()');
|
|
155
|
+
}, 120000);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('data loss', () => {
|
|
159
|
+
it('continues after a simulated mid-run failure without losing rows', async () => {
|
|
160
|
+
await migrateToJustBeforeBackfill();
|
|
161
|
+
const qi = harness.sequelize.getQueryInterface();
|
|
162
|
+
const customers = Array.from({ length: 5 }, (_, i) => ({
|
|
163
|
+
id: `cus_resume_${i}`,
|
|
164
|
+
livemode: 0,
|
|
165
|
+
did: `z-did-resume-${i}`,
|
|
166
|
+
delinquent: 0,
|
|
167
|
+
created_at: now(),
|
|
168
|
+
updated_at: now(),
|
|
169
|
+
}));
|
|
170
|
+
await qi.bulkInsert('customers', customers);
|
|
171
|
+
|
|
172
|
+
// simulate a crash that backfilled only part of the data
|
|
173
|
+
await harness.sequelize.query(
|
|
174
|
+
`UPDATE customers SET instance_did = '${getDefaultInstanceDid()}' WHERE id IN ('cus_resume_0', 'cus_resume_1')`
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
await harness.umzug.up(); // full run picks up the remaining NULL rows
|
|
178
|
+
expect(await countNulls(harness.sequelize, 'customers')).toBe(0);
|
|
179
|
+
const [rows] = await harness.sequelize.query('SELECT COUNT(*) AS n FROM customers');
|
|
180
|
+
expect((rows as any[])[0].n).toBe(5);
|
|
181
|
+
}, 120000);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('data damage', () => {
|
|
185
|
+
it('backfills payment_currencies from their payment_method tenant (D1)', async () => {
|
|
186
|
+
await migrateToJustBeforeBackfill();
|
|
187
|
+
const qi = harness.sequelize.getQueryInterface();
|
|
188
|
+
await qi.bulkInsert('payment_methods', [
|
|
189
|
+
{
|
|
190
|
+
id: 'pm_join',
|
|
191
|
+
livemode: 0,
|
|
192
|
+
active: 1,
|
|
193
|
+
confirmation: '{}',
|
|
194
|
+
settings: '{}',
|
|
195
|
+
features: '{}',
|
|
196
|
+
instance_did: TENANT_B, // pre-tagged method from another tenant
|
|
197
|
+
created_at: now(),
|
|
198
|
+
updated_at: now(),
|
|
199
|
+
},
|
|
200
|
+
]);
|
|
201
|
+
await qi.bulkInsert('payment_currencies', [
|
|
202
|
+
{
|
|
203
|
+
id: 'pc_join',
|
|
204
|
+
active: 1,
|
|
205
|
+
livemode: 0,
|
|
206
|
+
payment_method_id: 'pm_join',
|
|
207
|
+
name: 'Join Coin',
|
|
208
|
+
logo: 'http://x/l.png',
|
|
209
|
+
symbol: 'JC',
|
|
210
|
+
decimal: 8,
|
|
211
|
+
created_at: now(),
|
|
212
|
+
updated_at: now(),
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: 'pc_orphan',
|
|
216
|
+
active: 1,
|
|
217
|
+
livemode: 0,
|
|
218
|
+
payment_method_id: 'pm_missing',
|
|
219
|
+
name: 'Orphan Coin',
|
|
220
|
+
logo: 'http://x/l.png',
|
|
221
|
+
symbol: 'OC',
|
|
222
|
+
decimal: 8,
|
|
223
|
+
created_at: now(),
|
|
224
|
+
updated_at: now(),
|
|
225
|
+
},
|
|
226
|
+
]);
|
|
227
|
+
await harness.umzug.up();
|
|
228
|
+
const [rows] = await harness.sequelize.query(
|
|
229
|
+
"SELECT id, instance_did FROM payment_currencies WHERE id IN ('pc_join', 'pc_orphan') ORDER BY id"
|
|
230
|
+
);
|
|
231
|
+
const byId = Object.fromEntries((rows as any[]).map((r) => [r.id, r.instance_did]));
|
|
232
|
+
expect(byId.pc_join).toBe(TENANT_B); // follows its method
|
|
233
|
+
expect(byId.pc_orphan).toBe(getDefaultInstanceDid()); // dangling -> default
|
|
234
|
+
}, 120000);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe('data leak', () => {
|
|
238
|
+
it('rejects a duplicate promotion code within one tenant, allows it across tenants', async () => {
|
|
239
|
+
await harness.umzug.up();
|
|
240
|
+
const qi = harness.sequelize.getQueryInterface();
|
|
241
|
+
const promo = (id: string, tenant: string) => ({
|
|
242
|
+
id,
|
|
243
|
+
livemode: 0,
|
|
244
|
+
active: 1,
|
|
245
|
+
code: 'CODE1',
|
|
246
|
+
coupon_id: 'coup_x',
|
|
247
|
+
instance_did: tenant,
|
|
248
|
+
created_at: now(),
|
|
249
|
+
updated_at: now(),
|
|
250
|
+
});
|
|
251
|
+
await qi.bulkInsert('promotion_codes', [promo('promo_a', TENANT_A)]);
|
|
252
|
+
await qi.bulkInsert('promotion_codes', [promo('promo_b', TENANT_B)]); // cross-tenant ok
|
|
253
|
+
await expect(qi.bulkInsert('promotion_codes', [promo('promo_a2', TENANT_A)])).rejects.toThrow(
|
|
254
|
+
/unique|validation/i
|
|
255
|
+
);
|
|
256
|
+
const [rows] = await harness.sequelize.query("SELECT COUNT(*) AS n FROM promotion_codes WHERE code = 'CODE1'");
|
|
257
|
+
expect((rows as any[])[0].n).toBe(2);
|
|
258
|
+
}, 120000);
|
|
259
|
+
|
|
260
|
+
it('customers: same user DID may exist under two tenants after the rebuild', async () => {
|
|
261
|
+
await harness.umzug.up();
|
|
262
|
+
const qi = harness.sequelize.getQueryInterface();
|
|
263
|
+
const cust = (id: string, tenant: string) => ({
|
|
264
|
+
id,
|
|
265
|
+
livemode: 0,
|
|
266
|
+
did: 'z-shared-user',
|
|
267
|
+
delinquent: 0,
|
|
268
|
+
instance_did: tenant,
|
|
269
|
+
created_at: now(),
|
|
270
|
+
updated_at: now(),
|
|
271
|
+
});
|
|
272
|
+
await qi.bulkInsert('customers', [cust('cus_ta', TENANT_A)]);
|
|
273
|
+
await qi.bulkInsert('customers', [cust('cus_tb', TENANT_B)]);
|
|
274
|
+
await expect(qi.bulkInsert('customers', [cust('cus_ta2', TENANT_A)])).rejects.toThrow(/unique|validation/i);
|
|
275
|
+
}, 120000);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe('negative: backfill without phase 1', () => {
|
|
279
|
+
it('fails loudly with a missing-column error and writes nothing', async () => {
|
|
280
|
+
// migrate to BEFORE the phase 1 column migration: down twice
|
|
281
|
+
await harness.umzug.up();
|
|
282
|
+
await harness.umzug.down(); // revert backfill
|
|
283
|
+
await harness.umzug.down(); // revert tenant columns
|
|
284
|
+
await expect(runTenantBackfill(harness.sequelize)).rejects.toThrow(/instance_did missing/i);
|
|
285
|
+
}, 120000);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Sequelize } from 'sequelize';
|
|
2
|
+
|
|
3
|
+
import models, { Customer, initialize } from '../../src/store/models';
|
|
4
|
+
import { TENANT_TABLES } from '../../src/store/tenant-tables';
|
|
5
|
+
import { TENANT_A } from '../fixtures/tenants';
|
|
6
|
+
|
|
7
|
+
// Isolated from tenant-columns.spec.ts on purpose: initialize() mutates the
|
|
8
|
+
// model singletons (associations add FK clauses to attributes), which would
|
|
9
|
+
// corrupt the migration harness used there.
|
|
10
|
+
const sequelize = new Sequelize('sqlite::memory:', { logging: false });
|
|
11
|
+
initialize(sequelize);
|
|
12
|
+
afterAll(() => sequelize.close());
|
|
13
|
+
|
|
14
|
+
const MODEL_BY_TABLE = Object.fromEntries(Object.values(models).map((model: any) => [model.tableName, model]));
|
|
15
|
+
|
|
16
|
+
describe('tenant column model declarations (phase 1)', () => {
|
|
17
|
+
it('every tenant table model declares instance_did as a nullable attribute', () => {
|
|
18
|
+
for (const table of TENANT_TABLES) {
|
|
19
|
+
const model: any = MODEL_BY_TABLE[table];
|
|
20
|
+
expect({ table, hasModel: Boolean(model) }).toEqual({ table, hasModel: true });
|
|
21
|
+
const attribute = model.getAttributes().instance_did;
|
|
22
|
+
expect({ table, hasAttribute: Boolean(attribute) }).toEqual({ table, hasAttribute: true });
|
|
23
|
+
expect({ table, allowNull: attribute.allowNull !== false }).toEqual({ table, allowNull: true });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('model instances can read and write instance_did', () => {
|
|
28
|
+
const built = Customer.build({
|
|
29
|
+
livemode: false,
|
|
30
|
+
did: 'z-test-did',
|
|
31
|
+
delinquent: false,
|
|
32
|
+
instance_did: TENANT_A,
|
|
33
|
+
} as any);
|
|
34
|
+
expect(built.instance_did).toBe(TENANT_A);
|
|
35
|
+
built.instance_did = TENANT_A.replace('A', 'X');
|
|
36
|
+
expect(built.instance_did).toBe(TENANT_A.replace('A', 'X'));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('exempt tables do not gain the column', () => {
|
|
40
|
+
for (const table of ['jobs', 'locks', 'archive_locks', 'archive_metadata', 'exchange_rate_providers']) {
|
|
41
|
+
const model: any = MODEL_BY_TABLE[table];
|
|
42
|
+
expect({ table, hasModel: Boolean(model) }).toEqual({ table, hasModel: true });
|
|
43
|
+
expect({ table, attr: model.getAttributes().instance_did }).toEqual({ table, attr: undefined });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
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 { TENANT_TABLES } from '../../src/store/tenant-tables';
|
|
8
|
+
|
|
9
|
+
const STORE_DIR = path.join(__dirname, '../../src/store');
|
|
10
|
+
const TENANT_MIGRATION = '20260610-tenant-columns';
|
|
11
|
+
// pin the chain to phase 1: phase 2 (tenant backfill) intentionally rebuilds
|
|
12
|
+
// some tables with NOT NULL + composite uniques, which this spec must not see
|
|
13
|
+
const upToPhase1 = (umzug: Umzug<any>) => umzug.up({ to: `${TENANT_MIGRATION}.js` } as any);
|
|
14
|
+
|
|
15
|
+
function createHarness(storagePath: string) {
|
|
16
|
+
const sequelize = new Sequelize({ dialect: 'sqlite', storage: storagePath, logging: false });
|
|
17
|
+
const umzug = new Umzug({
|
|
18
|
+
migrations: {
|
|
19
|
+
glob: ['migrations/*.ts', { cwd: STORE_DIR }],
|
|
20
|
+
resolve: ({ name, path: migrationPath, context }) => {
|
|
21
|
+
// eslint-disable-next-line import/no-dynamic-require, global-require
|
|
22
|
+
const migration = require(migrationPath!);
|
|
23
|
+
return {
|
|
24
|
+
name: name.replace(/\.ts$/, '.js'),
|
|
25
|
+
up: () => migration.up({ context }),
|
|
26
|
+
down: () => migration.down({ context }),
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
context: sequelize.getQueryInterface(),
|
|
31
|
+
storage: new SequelizeStorage({ sequelize }),
|
|
32
|
+
logger: undefined,
|
|
33
|
+
});
|
|
34
|
+
return { sequelize, umzug };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function tableColumns(sequelize: Sequelize, table: string): Promise<Record<string, any>[]> {
|
|
38
|
+
const [rows] = await sequelize.query(`PRAGMA table_info(${table})`);
|
|
39
|
+
return rows as Record<string, any>[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('tenant columns migration (phase 1)', () => {
|
|
43
|
+
let dir: string;
|
|
44
|
+
let dbPath: string;
|
|
45
|
+
let harness: ReturnType<typeof createHarness>;
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tenant-columns-'));
|
|
49
|
+
dbPath = path.join(dir, 'test.db');
|
|
50
|
+
harness = createHarness(dbPath);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterEach(async () => {
|
|
54
|
+
await harness.sequelize.close();
|
|
55
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('happy path', () => {
|
|
59
|
+
it('adds a nullable instance_did column to all 38 tenant tables', async () => {
|
|
60
|
+
await upToPhase1(harness.umzug);
|
|
61
|
+
for (const table of TENANT_TABLES) {
|
|
62
|
+
// eslint-disable-next-line no-await-in-loop
|
|
63
|
+
const columns = await tableColumns(harness.sequelize, table);
|
|
64
|
+
const column = columns.find((c) => c.name === 'instance_did');
|
|
65
|
+
expect({ table, found: Boolean(column) }).toEqual({ table, found: true });
|
|
66
|
+
expect({ table, notnull: column!.notnull }).toEqual({ table, notnull: 0 });
|
|
67
|
+
expect({ table, dflt: column!.dflt_value ?? null }).toEqual({ table, dflt: null });
|
|
68
|
+
}
|
|
69
|
+
}, 60000);
|
|
70
|
+
|
|
71
|
+
// "model layer exposes instance_did" lives in tenant-columns-model.spec.ts:
|
|
72
|
+
// initializing the model singletons here would mutate GENESIS_ATTRIBUTES
|
|
73
|
+
// (associations add FK clauses) and corrupt the migration harness below.
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('bad input: re-running up is idempotent', () => {
|
|
77
|
+
it('skips existing columns without error and without duplicating them', async () => {
|
|
78
|
+
await upToPhase1(harness.umzug);
|
|
79
|
+
// call the migration's up() directly a second time, bypassing umzug bookkeeping
|
|
80
|
+
// eslint-disable-next-line import/no-dynamic-require, global-require
|
|
81
|
+
const migration = require(path.join(STORE_DIR, 'migrations', `${TENANT_MIGRATION}.ts`));
|
|
82
|
+
await migration.up({ context: harness.sequelize.getQueryInterface() });
|
|
83
|
+
const columns = await tableColumns(harness.sequelize, 'customers');
|
|
84
|
+
expect(columns.filter((c) => c.name === 'instance_did')).toHaveLength(1);
|
|
85
|
+
}, 60000);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('security: migration is built from static literals only', () => {
|
|
89
|
+
it('contains no env/config interpolation into SQL identifiers', () => {
|
|
90
|
+
const migrationSource = fs.readFileSync(path.join(STORE_DIR, 'migrations', `${TENANT_MIGRATION}.ts`), 'utf8');
|
|
91
|
+
expect(migrationSource).not.toContain('process.env');
|
|
92
|
+
expect(migrationSource).not.toContain('globalThis');
|
|
93
|
+
const listSource = fs.readFileSync(path.join(STORE_DIR, 'tenant-tables.ts'), 'utf8');
|
|
94
|
+
expect(listSource).not.toContain('process.env');
|
|
95
|
+
// every table entry is a quoted literal
|
|
96
|
+
const entries = listSource.match(/^\s*'[a-z_]+',$/gm) || [];
|
|
97
|
+
expect(entries.length).toBe(38);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('data loss / data damage: existing rows survive up and down', () => {
|
|
102
|
+
it('preserves pre-existing rows and their values through up, and through down', async () => {
|
|
103
|
+
// migrate to just before the tenant migration, then seed
|
|
104
|
+
await upToPhase1(harness.umzug);
|
|
105
|
+
await harness.umzug.down(); // revert the tenant column migration
|
|
106
|
+
|
|
107
|
+
const qi = harness.sequelize.getQueryInterface();
|
|
108
|
+
const now = new Date().toISOString();
|
|
109
|
+
await qi.bulkInsert('customers', [
|
|
110
|
+
{
|
|
111
|
+
id: 'cus_phase1_test1',
|
|
112
|
+
livemode: 0,
|
|
113
|
+
did: 'z-did-phase1-1',
|
|
114
|
+
delinquent: 0,
|
|
115
|
+
created_at: now,
|
|
116
|
+
updated_at: now,
|
|
117
|
+
},
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
// up: row survives, all original values intact, instance_did = NULL
|
|
121
|
+
await upToPhase1(harness.umzug);
|
|
122
|
+
const [afterUp] = await harness.sequelize.query(
|
|
123
|
+
"SELECT id, livemode, did, delinquent, instance_did FROM customers WHERE id = 'cus_phase1_test1'"
|
|
124
|
+
);
|
|
125
|
+
expect(afterUp).toHaveLength(1);
|
|
126
|
+
const row = (afterUp as any[])[0];
|
|
127
|
+
expect(row.did).toBe('z-did-phase1-1');
|
|
128
|
+
expect(row.livemode).toBe(0);
|
|
129
|
+
expect(row.instance_did).toBeNull();
|
|
130
|
+
|
|
131
|
+
// down: column removed, row and non-tenant values still intact
|
|
132
|
+
await harness.umzug.down();
|
|
133
|
+
const columns = await tableColumns(harness.sequelize, 'customers');
|
|
134
|
+
expect(columns.find((c) => c.name === 'instance_did')).toBeUndefined();
|
|
135
|
+
const [afterDown] = await harness.sequelize.query("SELECT id, did FROM customers WHERE id = 'cus_phase1_test1'");
|
|
136
|
+
expect(afterDown).toHaveLength(1);
|
|
137
|
+
expect((afterDown as any[])[0].did).toBe('z-did-phase1-1');
|
|
138
|
+
}, 120000);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('data leak: the new column is never auto-filled', () => {
|
|
142
|
+
it('defaults to NULL for rows written without an explicit tenant', async () => {
|
|
143
|
+
await upToPhase1(harness.umzug);
|
|
144
|
+
const qi = harness.sequelize.getQueryInterface();
|
|
145
|
+
const now = new Date().toISOString();
|
|
146
|
+
await qi.bulkInsert('products', [
|
|
147
|
+
{
|
|
148
|
+
id: 'prod_phase1_leak',
|
|
149
|
+
livemode: 0,
|
|
150
|
+
active: 1,
|
|
151
|
+
name: 'leak-check',
|
|
152
|
+
type: 'service',
|
|
153
|
+
created_at: now,
|
|
154
|
+
updated_at: now,
|
|
155
|
+
},
|
|
156
|
+
]);
|
|
157
|
+
const [rows] = await harness.sequelize.query("SELECT instance_did FROM products WHERE id = 'prod_phase1_leak'");
|
|
158
|
+
expect((rows as any[])[0].instance_did).toBeNull();
|
|
159
|
+
}, 60000);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -68,7 +68,14 @@ jest.mock('../../src/libs/queue', () => {
|
|
|
68
68
|
delete: jest.fn().mockResolvedValue(undefined),
|
|
69
69
|
on: jest.fn(),
|
|
70
70
|
};
|
|
71
|
-
|
|
71
|
+
const factory: any = jest.fn().mockReturnValue(mockQueue);
|
|
72
|
+
return {
|
|
73
|
+
__esModule: true,
|
|
74
|
+
default: factory,
|
|
75
|
+
// phase 5/6: tenant invariant helper — pass-through in these unit tests
|
|
76
|
+
// (tenant enforcement has its own suites: tenant-matrix-a/b)
|
|
77
|
+
assertJobObjectTenant: jest.fn(),
|
|
78
|
+
};
|
|
72
79
|
});
|
|
73
80
|
|
|
74
81
|
jest.mock('../../src/store/models', () => ({
|
|
@@ -71,7 +71,14 @@ jest.mock('../../src/libs/queue', () => {
|
|
|
71
71
|
delete: jest.fn().mockResolvedValue(undefined),
|
|
72
72
|
on: jest.fn(),
|
|
73
73
|
};
|
|
74
|
-
|
|
74
|
+
const factory: any = jest.fn().mockReturnValue(mockQueue);
|
|
75
|
+
return {
|
|
76
|
+
__esModule: true,
|
|
77
|
+
default: factory,
|
|
78
|
+
// phase 5/6: tenant invariant helper — pass-through in these unit tests
|
|
79
|
+
// (tenant enforcement has its own suites: tenant-matrix-a/b)
|
|
80
|
+
assertJobObjectTenant: jest.fn(),
|
|
81
|
+
};
|
|
75
82
|
});
|
|
76
83
|
|
|
77
84
|
jest.mock('../../src/store/models', () => ({
|