create-ailk 0.23.0 → 0.25.1
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/README.md +68 -33
- package/component-catalog.json +3 -3
- package/component-catalog.md +3 -3
- package/dist/add-module.js +10 -3
- package/dist/add-route.d.ts +12 -1
- package/dist/add-route.js +38 -4
- package/dist/cli.js +12 -7
- package/dist/derive-architecture.d.ts +3 -4
- package/dist/derive-architecture.js +75 -22
- package/dist/lib/design-checks.d.ts +9 -4
- package/dist/lib/design-checks.js +13 -7
- package/dist/lib/extract-module-bundle.js +29 -2
- package/dist/lib/fetch-template-snapshot.d.ts +1 -1
- package/dist/lib/fetch-template-snapshot.js +7 -3
- package/dist/lib/import-graph.d.ts +1 -1
- package/dist/lib/import-graph.js +4 -4
- package/dist/lib/module-license-gate.d.ts +12 -1
- package/dist/lib/module-license-gate.js +34 -12
- package/dist/lib/scaffold-render-sweep.d.ts +33 -0
- package/dist/lib/scaffold-render-sweep.js +45 -0
- package/dist/module-architecture.d.ts +20 -3
- package/dist/module-architecture.js +122 -538
- package/dist/paid-sections.d.ts +25 -1
- package/dist/paid-sections.js +88 -3
- package/dist/surfaces.js +10 -2
- package/dist/sync-routes.d.ts +30 -4
- package/dist/sync-routes.js +271 -18
- package/dist/template-record.d.ts +14 -4
- package/dist/template-record.js +23 -5
- package/dist/topology.d.ts +16 -3
- package/dist/topology.js +31 -19
- package/dist/update.d.ts +10 -1
- package/dist/update.js +540 -16
- package/package.json +3 -3
- package/release-scope.json +1 -1
- package/templates/.claude/cloudparity-repo-hooks.json +5 -0
- package/templates/.claude/hooks/template-guard.sh +137 -0
- package/templates/.claude/rules/extending.md +106 -0
- package/templates/.claude/rules/loop-contract.md +2 -2
- package/templates/.claude/settings.json +11 -0
- package/templates/.env.example +9 -5
- package/templates/.github/workflows/ci.yml +7 -4
- package/templates/CLAUDE.md +15 -3
- package/templates/CONVENTIONS.md +8 -10
- package/templates/README.md +14 -13
- package/templates/apps/api/.env.example +9 -2
- package/templates/apps/api/CLAUDE.md +122 -82
- package/templates/apps/api/jest.config.cjs +15 -5
- package/templates/apps/api/package.json +16 -20
- package/templates/apps/api/src/__tests__/trusted-origins.server.test.ts +2 -2
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +4 -4
- package/templates/apps/api/src/index.ts +1 -1
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +7 -4
- package/templates/apps/api/src/modules/commerce.ts +10 -0
- package/templates/apps/api/src/modules/content-write.ts +10 -0
- package/templates/apps/api/src/modules/multi-tenant.ts +10 -0
- package/templates/apps/api/src/modules/scheduling.ts +10 -0
- package/templates/apps/api/src/modules/signal-collectors.ts +10 -0
- package/templates/apps/api/src/modules/workspace.ts +10 -0
- package/templates/apps/api/src/server.ts +117 -1857
- package/templates/apps/api/vercel.json +2 -2
- package/templates/apps/mcp/CLAUDE.md +77 -24
- package/templates/apps/mcp/__tests__/app-entry.test.ts +4 -254
- package/templates/apps/mcp/__tests__/boot-file.test.ts +92 -0
- package/templates/apps/mcp/jest.config.cjs +6 -1
- package/templates/apps/mcp/package.json +5 -12
- package/templates/apps/mcp/src/app.ts +8 -7
- package/templates/apps/mcp/src/index.ts +6 -34
- package/templates/apps/mcp/src/lib/paid-tools.generated.ts +10 -0
- package/templates/apps/mcp/src/lib/topology.generated.ts +2 -2
- package/templates/apps/mcp/src/site.ts +12 -0
- package/templates/apps/web/.env.example +1 -1
- package/templates/apps/web/CLAUDE.md +21 -5
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +9 -3
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +14 -4
- package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +1 -1
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +22 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +28 -7
- package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +43 -0
- package/templates/apps/web/app/[locale]/pricing/page.tsx +7 -3
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/__tests__/route.test.ts +222 -0
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +10 -3
- package/templates/apps/web/app/api/revalidate/route.ts +3 -3
- package/templates/apps/web/auth.ts +2 -2
- package/templates/apps/web/jest.config.cjs +1 -0
- package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +7 -0
- package/templates/apps/web/lib/auth-email.ts +1 -1
- package/templates/apps/web/lib/magnet-routes.ts +3 -1
- package/templates/apps/web/lib/newsletter.actions.ts +9 -6
- package/templates/apps/web/lib/relay.ts +2 -2
- package/templates/apps/web/lib/release-scope.ts +47 -0
- package/templates/apps/web/lib/server-api.ts +1 -1
- package/templates/apps/web/src/lib/commerce-claim.ts +1 -1
- package/templates/customizations.yaml +37 -0
- package/templates/database/prisma.config.ts +10 -14
- package/templates/database/seed-test.ts +2 -2
- package/templates/database/seed.ts +32 -5
- package/templates/package.json +12 -6
- package/templates/pnpm-workspace.yaml +0 -1
- package/templates/project.yaml +16 -8
- package/templates/tools/jest/reporters.cjs +65 -0
- package/templates/tools/lint-architecture.ts +757 -7
- package/templates/tsconfig.json +1 -2
- package/versions.json +48 -35
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +0 -409
- package/templates/apps/api/src/__tests__/factories/agent-caller.ts +0 -102
- package/templates/apps/api/src/__tests__/factories/content-fixture.ts +0 -138
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +0 -441
- package/templates/apps/api/src/__tests__/factories/index.ts +0 -70
- package/templates/apps/api/src/__tests__/factories/lead-fixture.ts +0 -82
- package/templates/apps/api/src/__tests__/leads-forwarded-rate-limit.test.ts +0 -135
- package/templates/apps/api/src/__tests__/services/enrichment-shim.test.ts +0 -126
- package/templates/apps/api/src/__tests__/stripe-boot.test.ts +0 -132
- package/templates/apps/api/src/__tests__/support/hub-sign-in-fake-db.ts +0 -362
- package/templates/apps/api/src/bin/__tests__/rate-limit-probe.test.ts +0 -184
- package/templates/apps/api/src/bin/abandon-sweep.ts +0 -42
- package/templates/apps/api/src/bin/rate-limit-probe.ts +0 -53
- package/templates/apps/api/src/bin/spokes.ts +0 -198
- package/templates/apps/api/src/bin/stripe-connections-rekey.ts +0 -45
- package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +0 -196
- package/templates/apps/api/src/config/__tests__/index.test.ts +0 -233
- package/templates/apps/api/src/config/__tests__/modules.test.ts +0 -76
- package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +0 -185
- package/templates/apps/api/src/config/__tests__/trusted-origins.test.ts +0 -238
- package/templates/apps/api/src/config/index.ts +0 -596
- package/templates/apps/api/src/config/modules.ts +0 -83
- package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +0 -581
- package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +0 -143
- package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +0 -237
- package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +0 -353
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +0 -201
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +0 -224
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +0 -120
- package/templates/apps/api/src/lib/__tests__/errors.test.ts +0 -94
- package/templates/apps/api/src/lib/__tests__/file-fallback.test.ts +0 -144
- package/templates/apps/api/src/lib/__tests__/fixtures/HomePage/home.mdx +0 -8
- package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +0 -149
- package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +0 -116
- package/templates/apps/api/src/lib/__tests__/hub-sign-in-store.test.ts +0 -209
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +0 -257
- package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +0 -96
- package/templates/apps/api/src/lib/__tests__/outbound-http.test.ts +0 -125
- package/templates/apps/api/src/lib/__tests__/prisma.test.ts +0 -165
- package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +0 -317
- package/templates/apps/api/src/lib/__tests__/require-paid-tier.test.ts +0 -129
- package/templates/apps/api/src/lib/__tests__/site.test.ts +0 -292
- package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +0 -89
- package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +0 -202
- package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +0 -217
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -82
- package/templates/apps/api/src/lib/agent-store.ts +0 -613
- package/templates/apps/api/src/lib/auth-email-budget.ts +0 -161
- package/templates/apps/api/src/lib/consent-store.ts +0 -363
- package/templates/apps/api/src/lib/content-adapter.ts +0 -112
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +0 -96
- package/templates/apps/api/src/lib/error-serializer.ts +0 -164
- package/templates/apps/api/src/lib/errors.ts +0 -95
- package/templates/apps/api/src/lib/file-fallback.ts +0 -87
- package/templates/apps/api/src/lib/flow-completion-store.ts +0 -136
- package/templates/apps/api/src/lib/hub-sign-in-store.ts +0 -455
- package/templates/apps/api/src/lib/lead-capture-store.ts +0 -122
- package/templates/apps/api/src/lib/lead-delivery.ts +0 -52
- package/templates/apps/api/src/lib/license-key-store.ts +0 -45
- package/templates/apps/api/src/lib/locales.ts +0 -201
- package/templates/apps/api/src/lib/outbound-http.ts +0 -127
- package/templates/apps/api/src/lib/prisma.ts +0 -21
- package/templates/apps/api/src/lib/proposal-paid-store.ts +0 -66
- package/templates/apps/api/src/lib/rate-limit-probe.ts +0 -165
- package/templates/apps/api/src/lib/rate-limit-store.ts +0 -142
- package/templates/apps/api/src/lib/require-commerce-claim.ts +0 -250
- package/templates/apps/api/src/lib/require-paid-tier.ts +0 -53
- package/templates/apps/api/src/lib/site.ts +0 -254
- package/templates/apps/api/src/lib/slice-load.ts +0 -90
- package/templates/apps/api/src/lib/spoke-store.ts +0 -180
- package/templates/apps/api/src/lib/stripe.ts +0 -318
- package/templates/apps/api/src/lib/webhook-idempotency.ts +0 -156
- package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +0 -111
- package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +0 -172
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +0 -119
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +0 -138
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-trusted-origins.test.ts +0 -142
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +0 -155
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +0 -139
- package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +0 -562
- package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +0 -76
- package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +0 -220
- package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +0 -97
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +0 -557
- package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +0 -72
- package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +0 -140
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +0 -721
- package/templates/apps/api/src/middleware/__tests__/rate-limit-cross-isolate.db.test.ts +0 -220
- package/templates/apps/api/src/middleware/__tests__/rate-limit-store.test.ts +0 -430
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +0 -1409
- package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +0 -174
- package/templates/apps/api/src/middleware/auth.ts +0 -864
- package/templates/apps/api/src/middleware/default-tenant.ts +0 -166
- package/templates/apps/api/src/middleware/rate-limit.ts +0 -983
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +0 -264
- package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +0 -182
- package/templates/apps/api/src/openapi/index.ts +0 -18
- package/templates/apps/api/src/openapi/spec.ts +0 -1815
- package/templates/apps/api/src/openapi/surface-drift.ts +0 -195
- package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +0 -175
- package/templates/apps/api/src/routes/aeo/index.ts +0 -107
- package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +0 -196
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +0 -249
- package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +0 -532
- package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +0 -313
- package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +0 -413
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +0 -609
- package/templates/apps/api/src/routes/auth/hub-sign-in.ts +0 -242
- package/templates/apps/api/src/routes/auth/tokens.ts +0 -211
- package/templates/apps/api/src/routes/auth/transactional-email.ts +0 -301
- package/templates/apps/api/src/routes/auth/user.ts +0 -305
- package/templates/apps/api/src/routes/billing/plan-checkout.ts +0 -325
- package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +0 -178
- package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +0 -1300
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +0 -448
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -1742
- package/templates/apps/api/src/routes/checkout/index.ts +0 -34
- package/templates/apps/api/src/routes/checkout/session.ts +0 -171
- package/templates/apps/api/src/routes/checkout/sessions.ts +0 -540
- package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +0 -386
- package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +0 -160
- package/templates/apps/api/src/routes/consent/account.ts +0 -268
- package/templates/apps/api/src/routes/consent/cookie.ts +0 -90
- package/templates/apps/api/src/routes/consent/index.ts +0 -26
- package/templates/apps/api/src/routes/consent/unsubscribe.ts +0 -127
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +0 -853
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +0 -230
- package/templates/apps/api/src/routes/content/__tests__/list-types.test.ts +0 -44
- package/templates/apps/api/src/routes/content/__tests__/locale.test.ts +0 -118
- package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +0 -731
- package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +0 -753
- package/templates/apps/api/src/routes/content/__tests__/rate-limit.test.ts +0 -51
- package/templates/apps/api/src/routes/content/__tests__/search.test.ts +0 -450
- package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +0 -762
- package/templates/apps/api/src/routes/content/get-page.ts +0 -268
- package/templates/apps/api/src/routes/content/index.ts +0 -49
- package/templates/apps/api/src/routes/content/list-pages.ts +0 -95
- package/templates/apps/api/src/routes/content/list-types.ts +0 -19
- package/templates/apps/api/src/routes/content/offerings.ts +0 -184
- package/templates/apps/api/src/routes/content/pricing.ts +0 -176
- package/templates/apps/api/src/routes/content/rate-limit.ts +0 -22
- package/templates/apps/api/src/routes/content/search.ts +0 -129
- package/templates/apps/api/src/routes/content/shown-offerings.ts +0 -229
- package/templates/apps/api/src/routes/content/slug.ts +0 -9
- package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +0 -88
- package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +0 -171
- package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +0 -515
- package/templates/apps/api/src/routes/leads/__tests__/spoke.test.ts +0 -392
- package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +0 -165
- package/templates/apps/api/src/routes/leads/index.ts +0 -573
- package/templates/apps/api/src/routes/leads/list.ts +0 -104
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +0 -626
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +0 -1268
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +0 -847
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +0 -480
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +0 -465
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +0 -872
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +0 -1022
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +0 -484
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +0 -535
- package/templates/apps/api/src/routes/project-listings/index.ts +0 -164
- package/templates/apps/api/src/routes/project-listings/me.ts +0 -87
- package/templates/apps/api/src/routes/project-listings/patch-copy.ts +0 -224
- package/templates/apps/api/src/routes/project-listings/patch-draft.ts +0 -102
- package/templates/apps/api/src/routes/project-listings/public.ts +0 -98
- package/templates/apps/api/src/routes/project-listings/respond.ts +0 -48
- package/templates/apps/api/src/routes/project-listings/resume-token.ts +0 -27
- package/templates/apps/api/src/routes/project-listings/start.ts +0 -140
- package/templates/apps/api/src/routes/project-listings/submit.ts +0 -130
- package/templates/apps/api/src/routes/project-listings/verify-email.ts +0 -150
- package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +0 -686
- package/templates/apps/api/src/routes/requests/confirm.ts +0 -601
- package/templates/apps/api/src/routes/requests/index.ts +0 -54
- package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +0 -306
- package/templates/apps/api/src/routes/test/auth/session.ts +0 -143
- package/templates/apps/api/src/routes/webhooks/README.md +0 -398
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +0 -649
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +0 -749
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -448
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -264
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +0 -159
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +0 -158
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +0 -814
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -301
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-proposal-paid.test.ts +0 -369
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -215
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +0 -301
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +0 -808
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -1155
- package/templates/apps/api/src/routes/webhooks/index.ts +0 -141
- package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +0 -384
- package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -2026
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +0 -528
- package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +0 -132
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +0 -1028
- package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +0 -313
- package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +0 -503
- package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +0 -65
- package/templates/apps/api/src/services/__tests__/consent.test.ts +0 -299
- package/templates/apps/api/src/services/__tests__/default-tenant.test.ts +0 -147
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +0 -867
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +0 -379
- package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +0 -368
- package/templates/apps/api/src/services/__tests__/entitlements.test.ts +0 -142
- package/templates/apps/api/src/services/__tests__/hub-sign-in.test.ts +0 -854
- package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +0 -234
- package/templates/apps/api/src/services/__tests__/lead-accept-user.test.ts +0 -251
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +0 -2660
- package/templates/apps/api/src/services/__tests__/listing-config.test.ts +0 -166
- package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +0 -389
- package/templates/apps/api/src/services/__tests__/person.test.ts +0 -1176
- package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +0 -444
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +0 -154
- package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +0 -137
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +0 -531
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +0 -372
- package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +0 -348
- package/templates/apps/api/src/services/__tests__/spoke-resolution.test.ts +0 -660
- package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +0 -941
- package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +0 -588
- package/templates/apps/api/src/services/__tests__/verified-email.test.ts +0 -78
- package/templates/apps/api/src/services/abandon-email.ts +0 -313
- package/templates/apps/api/src/services/aeo-score.ts +0 -325
- package/templates/apps/api/src/services/answer-distributions.ts +0 -138
- package/templates/apps/api/src/services/audit-log.ts +0 -961
- package/templates/apps/api/src/services/checkout-buyer.ts +0 -168
- package/templates/apps/api/src/services/consent.ts +0 -834
- package/templates/apps/api/src/services/default-tenant.ts +0 -416
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +0 -755
- package/templates/apps/api/src/services/dunning-policy.ts +0 -294
- package/templates/apps/api/src/services/enrichment-shim.ts +0 -53
- package/templates/apps/api/src/services/entitlements.ts +0 -338
- package/templates/apps/api/src/services/hub-sign-in.ts +0 -472
- package/templates/apps/api/src/services/lead-accept-user.ts +0 -126
- package/templates/apps/api/src/services/lead-promotion.ts +0 -160
- package/templates/apps/api/src/services/lead-routing.ts +0 -1372
- package/templates/apps/api/src/services/leads-inbox.ts +0 -312
- package/templates/apps/api/src/services/listing-config.ts +0 -178
- package/templates/apps/api/src/services/magnet-completion.ts +0 -242
- package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +0 -295
- package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +0 -268
- package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +0 -868
- package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +0 -416
- package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +0 -396
- package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +0 -681
- package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +0 -231
- package/templates/apps/api/src/services/offerings/catalog.ts +0 -308
- package/templates/apps/api/src/services/offerings/checkout-authorization.ts +0 -385
- package/templates/apps/api/src/services/offerings/checkout-session-record.ts +0 -192
- package/templates/apps/api/src/services/offerings/connection.ts +0 -1003
- package/templates/apps/api/src/services/offerings/index.ts +0 -614
- package/templates/apps/api/src/services/offerings/source.ts +0 -140
- package/templates/apps/api/src/services/offerings/store.ts +0 -150
- package/templates/apps/api/src/services/owner-workspaces.ts +0 -132
- package/templates/apps/api/src/services/person.ts +0 -1188
- package/templates/apps/api/src/services/plan-offerings.ts +0 -125
- package/templates/apps/api/src/services/project-listing-resume-email.ts +0 -326
- package/templates/apps/api/src/services/project-listings.ts +0 -2073
- package/templates/apps/api/src/services/public-site-resolver.ts +0 -396
- package/templates/apps/api/src/services/request-confirm.ts +0 -698
- package/templates/apps/api/src/services/revalidation.ts +0 -235
- package/templates/apps/api/src/services/slack-notify.ts +0 -291
- package/templates/apps/api/src/services/usage-aggregation.ts +0 -516
- package/templates/apps/api/src/services/usage-credits.ts +0 -340
- package/templates/apps/api/src/services/verified-email.ts +0 -71
- package/templates/apps/mcp/__tests__/abuse/daily-cap.test.ts +0 -52
- package/templates/apps/mcp/__tests__/abuse/email-validator.test.ts +0 -46
- package/templates/apps/mcp/__tests__/abuse/guard.test.ts +0 -198
- package/templates/apps/mcp/__tests__/abuse/honeypot.test.ts +0 -36
- package/templates/apps/mcp/__tests__/abuse/user-agent-deny.test.ts +0 -54
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +0 -116
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +0 -299
- package/templates/apps/mcp/__tests__/boot.test.ts +0 -280
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +0 -359
- package/templates/apps/mcp/__tests__/config/modules.test.ts +0 -78
- package/templates/apps/mcp/__tests__/get_business_hours.test.ts +0 -82
- package/templates/apps/mcp/__tests__/get_faq.test.ts +0 -80
- package/templates/apps/mcp/__tests__/get_locations.test.ts +0 -65
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +0 -557
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +0 -524
- package/templates/apps/mcp/__tests__/get_site_info.test.ts +0 -81
- package/templates/apps/mcp/__tests__/http-transport.test.ts +0 -1151
- package/templates/apps/mcp/__tests__/index.test.ts +0 -195
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +0 -191
- package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +0 -531
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +0 -65
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -187
- package/templates/apps/mcp/__tests__/lib/content-root.test.ts +0 -93
- package/templates/apps/mcp/__tests__/lib/errors.test.ts +0 -75
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +0 -165
- package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +0 -124
- package/templates/apps/mcp/__tests__/list_pages.test.ts +0 -228
- package/templates/apps/mcp/__tests__/locale.test.ts +0 -135
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +0 -175
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +0 -101
- package/templates/apps/mcp/__tests__/read_content.test.ts +0 -474
- package/templates/apps/mcp/__tests__/request_callback.test.ts +0 -225
- package/templates/apps/mcp/__tests__/search_content.test.ts +0 -256
- package/templates/apps/mcp/__tests__/server.test.ts +0 -388
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +0 -448
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +0 -307
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +0 -223
- package/templates/apps/mcp/__tests__/tools/create_project.test.ts +0 -243
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +0 -451
- package/templates/apps/mcp/src/abuse/daily-cap.ts +0 -47
- package/templates/apps/mcp/src/abuse/email-validator.ts +0 -25
- package/templates/apps/mcp/src/abuse/honeypot.ts +0 -21
- package/templates/apps/mcp/src/abuse/index.ts +0 -342
- package/templates/apps/mcp/src/abuse/user-agent-deny.ts +0 -58
- package/templates/apps/mcp/src/boot.ts +0 -167
- package/templates/apps/mcp/src/config/modules.ts +0 -43
- package/templates/apps/mcp/src/http-transport.ts +0 -526
- package/templates/apps/mcp/src/lib/audit-single-page.ts +0 -308
- package/templates/apps/mcp/src/lib/commerce-claim.ts +0 -263
- package/templates/apps/mcp/src/lib/content-adapter.ts +0 -142
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +0 -139
- package/templates/apps/mcp/src/lib/errors.ts +0 -61
- package/templates/apps/mcp/src/lib/license-key-store.ts +0 -45
- package/templates/apps/mcp/src/lib/locales.ts +0 -171
- package/templates/apps/mcp/src/lib/prisma.ts +0 -39
- package/templates/apps/mcp/src/lib/public-workspace.ts +0 -133
- package/templates/apps/mcp/src/lib/resolve-locale.ts +0 -30
- package/templates/apps/mcp/src/lib/shown-offerings.ts +0 -105
- package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +0 -59
- package/templates/apps/mcp/src/request-context.ts +0 -52
- package/templates/apps/mcp/src/server.ts +0 -343
- package/templates/apps/mcp/src/serverless-app.ts +0 -88
- package/templates/apps/mcp/src/tools/audit.ts +0 -127
- package/templates/apps/mcp/src/tools/capture_lead.ts +0 -64
- package/templates/apps/mcp/src/tools/create_project.ts +0 -134
- package/templates/apps/mcp/src/tools/get_business_hours.ts +0 -68
- package/templates/apps/mcp/src/tools/get_faq.ts +0 -71
- package/templates/apps/mcp/src/tools/get_locations.ts +0 -88
- package/templates/apps/mcp/src/tools/get_offerings.ts +0 -96
- package/templates/apps/mcp/src/tools/get_pricing.ts +0 -94
- package/templates/apps/mcp/src/tools/get_site_info.ts +0 -86
- package/templates/apps/mcp/src/tools/index.ts +0 -137
- package/templates/apps/mcp/src/tools/list_pages.ts +0 -74
- package/templates/apps/mcp/src/tools/person_notes.ts +0 -25
- package/templates/apps/mcp/src/tools/proposals.ts +0 -27
- package/templates/apps/mcp/src/tools/purchase.ts +0 -205
- package/templates/apps/mcp/src/tools/read_content.ts +0 -75
- package/templates/apps/mcp/src/tools/request_callback.ts +0 -59
- package/templates/apps/mcp/src/tools/search_content.ts +0 -67
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +0 -57
- package/templates/apps/mcp/src/workspace-server.ts +0 -224
- package/templates/database/CHANGELOG.md +0 -961
- package/templates/database/LICENSE +0 -202
- package/templates/database/NOTICE +0 -2
- package/templates/database/README.md +0 -243
- package/templates/database/__tests__/agents.db.test.ts +0 -320
- package/templates/database/__tests__/appointment.db.test.ts +0 -106
- package/templates/database/__tests__/artifact-content-store.db.test.ts +0 -316
- package/templates/database/__tests__/composite-fence.db.test.ts +0 -206
- package/templates/database/__tests__/consent-ledger.db.test.ts +0 -345
- package/templates/database/__tests__/consent-source-checkout-migration.test.ts +0 -79
- package/templates/database/__tests__/fence-fan-out.db.test.ts +0 -715
- package/templates/database/__tests__/fixtures/appointment-create-probe.ts +0 -79
- package/templates/database/__tests__/fixtures/person-backfill.json +0 -264
- package/templates/database/__tests__/fixtures/person-contact-backfill.json +0 -182
- package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +0 -100
- package/templates/database/__tests__/flow-completion-migration.test.ts +0 -115
- package/templates/database/__tests__/flow-completion.db.test.ts +0 -136
- package/templates/database/__tests__/flow-workspace-key.db.test.ts +0 -263
- package/templates/database/__tests__/flows-listing-diagnostic.test.ts +0 -392
- package/templates/database/__tests__/flows-presets.test.ts +0 -173
- package/templates/database/__tests__/in-page-magnets-migration.test.ts +0 -161
- package/templates/database/__tests__/lead-idempotency-key.db.test.ts +0 -92
- package/templates/database/__tests__/member-area.db.test.ts +0 -257
- package/templates/database/__tests__/member-contract-migration.test.ts +0 -85
- package/templates/database/__tests__/member-contract.db.test.ts +0 -413
- package/templates/database/__tests__/offerings-checkout-session.db.test.ts +0 -270
- package/templates/database/__tests__/offerings-migration.test.ts +0 -197
- package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +0 -86
- package/templates/database/__tests__/offerings.db.test.ts +0 -303
- package/templates/database/__tests__/outbound-message-action.db.test.ts +0 -80
- package/templates/database/__tests__/person-contact.db.test.ts +0 -563
- package/templates/database/__tests__/person-notes.db.test.ts +0 -249
- package/templates/database/__tests__/person.db.test.ts +0 -586
- package/templates/database/__tests__/piece-kind-migration.test.ts +0 -67
- package/templates/database/__tests__/piece-kind.db.test.ts +0 -208
- package/templates/database/__tests__/prefixed-ids.db.test.ts +0 -222
- package/templates/database/__tests__/project-listing-consent-at.db.test.ts +0 -111
- package/templates/database/__tests__/seed.test.ts +0 -240
- package/templates/database/__tests__/spoke-registry.db.test.ts +0 -191
- package/templates/database/__tests__/tenancy.test.ts +0 -141
- package/templates/database/__tests__/user-cascade.test.ts +0 -140
- package/templates/database/_gitignore +0 -8
- package/templates/database/auth/.gitkeep +0 -0
- package/templates/database/auth/schema.prisma +0 -641
- package/templates/database/config/.gitkeep +0 -0
- package/templates/database/config/schema.prisma +0 -441
- package/templates/database/content/.gitkeep +0 -0
- package/templates/database/content/schema.prisma +0 -339
- package/templates/database/eslint.config.mjs +0 -69
- package/templates/database/flows/listing-diagnostic.ts +0 -182
- package/templates/database/flows/presets.ts +0 -315
- package/templates/database/inbox/.gitkeep +0 -0
- package/templates/database/inbox/README.md +0 -3
- package/templates/database/inbox/__tests__/schema.test.ts +0 -44
- package/templates/database/inbox/schema.prisma +0 -1447
- package/templates/database/jest.config.cjs +0 -36
- package/templates/database/migrations/20260508000000_add_inbox_namespace/migration.sql +0 -328
- package/templates/database/migrations/20260513000000_better_auth_migration/migration.sql +0 -100
- package/templates/database/migrations/20260515000000_add_user_billing_fields/migration.sql +0 -28
- package/templates/database/migrations/20260516000000_add_user_tier_care_plan_seat/migration.sql +0 -25
- package/templates/database/migrations/20260518000000_add_telemetry_records/migration.sql +0 -42
- package/templates/database/migrations/20260525000000_theme_wiring/migration.sql +0 -67
- package/templates/database/migrations/20260710000000_retire_commercial_tier/migration.sql +0 -21
- package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +0 -201
- package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +0 -67
- package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +0 -46
- package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +0 -25
- package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +0 -49
- package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +0 -197
- package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +0 -32
- package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +0 -22
- package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +0 -151
- package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +0 -223
- package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +0 -22
- package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +0 -32
- package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +0 -40
- package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +0 -4
- package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +0 -2
- package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +0 -2
- package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +0 -29
- package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +0 -6
- package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +0 -39
- package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +0 -24
- package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +0 -3
- package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +0 -33
- package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +0 -30
- package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +0 -47
- package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +0 -16
- package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +0 -47
- package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +0 -10
- package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +0 -26
- package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +0 -90
- package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +0 -174
- package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +0 -14
- package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +0 -25
- package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +0 -32
- package/templates/database/migrations/20260911180000_consent_grants/migration.sql +0 -71
- package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +0 -30
- package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +0 -49
- package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +0 -123
- package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +0 -15
- package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +0 -158
- package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +0 -40
- package/templates/database/migrations/20260918120000_add_appointment/migration.sql +0 -67
- package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +0 -70
- package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +0 -181
- package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +0 -172
- package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +0 -41
- package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +0 -185
- package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +0 -70
- package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +0 -57
- package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +0 -68
- package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +0 -1067
- package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +0 -210
- package/templates/database/migrations/20261003120000_spoke_registry/migration.sql +0 -80
- package/templates/database/migrations/20261003130000_scheduling_connection/migration.sql +0 -49
- package/templates/database/migrations/20261005120000_agents/migration.sql +0 -84
- package/templates/database/migrations/20261008120000_user_last_workspace/migration.sql +0 -35
- package/templates/database/migrations/20261008130000_lead_idempotency_key/migration.sql +0 -20
- package/templates/database/migrations/20261009120000_agent_grants/migration.sql +0 -33
- package/templates/database/migrations/20261010120000_person/migration.sql +0 -106
- package/templates/database/migrations/20261010121000_person_backfill/migration.sql +0 -132
- package/templates/database/migrations/20261010130000_agent_grants_people_read/migration.sql +0 -20
- package/templates/database/migrations/20261011120000_member_area/migration.sql +0 -158
- package/templates/database/migrations/20261011130000_agent_grants_member_pieces_manage/migration.sql +0 -21
- package/templates/database/migrations/20261012120000_member_contract/migration.sql +0 -192
- package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +0 -20
- package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +0 -122
- package/templates/database/migrations/20261014120000_person_contact/migration.sql +0 -231
- package/templates/database/migrations/20261015120000_person_notes/migration.sql +0 -77
- package/templates/database/migrations/20261015120000_rate_limit_counters/migration.sql +0 -43
- package/templates/database/migrations/20261016120000_piece_kind/migration.sql +0 -95
- package/templates/database/migrations/20261017120000_outbound_message_action/migration.sql +0 -30
- package/templates/database/migrations/20261018120000_flow_completion/migration.sql +0 -52
- package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +0 -27
- package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +0 -146
- package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +0 -77
- package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +0 -45
- package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +0 -47
- package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +0 -16
- package/templates/database/migrations/migration_lock.toml +0 -3
- package/templates/database/ops/.gitkeep +0 -0
- package/templates/database/ops/schema.prisma +0 -43
- package/templates/database/package.json +0 -89
- package/templates/database/schema.prisma +0 -71
- package/templates/database/scripts/db-generate-locked.sh +0 -80
- package/templates/database/scripts/person-backfill.sql +0 -132
- package/templates/database/tenancy.ts +0 -183
- package/templates/database/tsconfig.build.json +0 -16
- package/templates/database/tsconfig.flows.json +0 -15
- package/templates/database/tsconfig.json +0 -16
- package/templates/database/tsconfig.tenancy.json +0 -14
|
@@ -1,681 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* services/offerings — the read services and the bounded refresh (#8876 spec
|
|
3
|
-
* D7, D8, D9, D11): the typed shape (AC-3), a shown set built with no Stripe
|
|
4
|
-
* call (AC-6), the 24-hour last-good copy (AC-7), the refresh bound (AC-8),
|
|
5
|
-
* checkout's allowlist never reading the snapshot (AC-10) and nothing
|
|
6
|
-
* leaking into a log or an error (AC-12).
|
|
7
|
-
*
|
|
8
|
-
* Every suite runs over `createInMemoryPrisma()` and the D9 seed, with
|
|
9
|
-
* `@working-theory/stripe-client` mocked so that touching any of its exports
|
|
10
|
-
* throws.
|
|
11
|
-
*/
|
|
12
|
-
// eslint-disable-next-line no-restricted-syntax -- lib/prisma.ts wraps the external DB boundary (PrismaClient requires a live connection); the in-memory double is the repo-standard where-honouring test double for it.
|
|
13
|
-
jest.mock("../../../lib/prisma.js", () =>
|
|
14
|
-
jest
|
|
15
|
-
.requireActual<typeof import("../../../__tests__/factories/in-memory-prisma")>(
|
|
16
|
-
"../../../__tests__/factories/in-memory-prisma",
|
|
17
|
-
)
|
|
18
|
-
.createInMemoryPrisma(),
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
jest.mock(
|
|
22
|
-
"@working-theory/stripe-client",
|
|
23
|
-
() =>
|
|
24
|
-
new Proxy(
|
|
25
|
-
{},
|
|
26
|
-
{
|
|
27
|
-
get: (_target, key) => {
|
|
28
|
-
if (key === "__esModule") return true;
|
|
29
|
-
throw new Error(`@working-theory/stripe-client.${String(key)} was touched`);
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
),
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
const mockLog = jest.fn();
|
|
36
|
-
jest.mock("@working-theory/observability", () => ({
|
|
37
|
-
logger: {
|
|
38
|
-
error: (...args: unknown[]) => mockLog("error", ...args),
|
|
39
|
-
warn: (...args: unknown[]) => mockLog("warn", ...args),
|
|
40
|
-
info: (...args: unknown[]) => mockLog("info", ...args),
|
|
41
|
-
debug: (...args: unknown[]) => mockLog("debug", ...args),
|
|
42
|
-
},
|
|
43
|
-
}));
|
|
44
|
-
|
|
45
|
-
import {
|
|
46
|
-
offeringsSnapshotSchema,
|
|
47
|
-
orgEnabledProductsSchema,
|
|
48
|
-
workspaceShownSetSchema,
|
|
49
|
-
} from "@working-theory/validation";
|
|
50
|
-
|
|
51
|
-
import { prisma } from "../../../lib/prisma.js";
|
|
52
|
-
import {
|
|
53
|
-
LAST_GOOD_MAX_AGE_MS,
|
|
54
|
-
OfferingsShapeError,
|
|
55
|
-
OfferingsSourceError,
|
|
56
|
-
getOrgEnabledProducts,
|
|
57
|
-
getSellableProductIds,
|
|
58
|
-
getWorkspaceShownSet,
|
|
59
|
-
refreshWorkspaceSnapshot,
|
|
60
|
-
resetOfferingsRefreshStateForTests,
|
|
61
|
-
shownPriceIdsFor,
|
|
62
|
-
type OfferingsSourceFor,
|
|
63
|
-
} from "../index.js";
|
|
64
|
-
import {
|
|
65
|
-
EXPECTED_FIXTURE_HELD,
|
|
66
|
-
EXPECTED_FIXTURE_OFFERINGS,
|
|
67
|
-
EXPECTED_SHOWN_SET,
|
|
68
|
-
FIXTURE_CIPHERTEXT_PLACEHOLDER,
|
|
69
|
-
FIXTURE_ENABLED_PRODUCT_IDS,
|
|
70
|
-
FIXTURE_KEY_LAST4,
|
|
71
|
-
FIXTURE_NOW,
|
|
72
|
-
FIXTURE_ORG,
|
|
73
|
-
FIXTURE_ORG_2,
|
|
74
|
-
FIXTURE_ORG_2_WS,
|
|
75
|
-
FIXTURE_SHOWN_PRODUCT_IDS,
|
|
76
|
-
FIXTURE_WEBHOOK_TOKEN,
|
|
77
|
-
FIXTURE_WS,
|
|
78
|
-
FIXTURE_WS_2,
|
|
79
|
-
OFFERINGS_FIXTURE_CATALOG,
|
|
80
|
-
fixtureSourceFor,
|
|
81
|
-
refusingSourceFor,
|
|
82
|
-
seedOfferings,
|
|
83
|
-
} from "./fixtures.js";
|
|
84
|
-
|
|
85
|
-
type Row = Record<string, unknown>;
|
|
86
|
-
// The in-memory double exposes its store and recorded delegates, which the
|
|
87
|
-
// PrismaClient type the import carries does not name, so it is named here once.
|
|
88
|
-
// eslint-disable-next-line no-restricted-syntax -- justified above
|
|
89
|
-
const db = prisma as unknown as {
|
|
90
|
-
__db: Record<string, Row[]>;
|
|
91
|
-
workspaceOfferings: { findUnique: jest.Mock; create: (args: { data: Row }) => Promise<Row> };
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const SECOND = 1000;
|
|
95
|
-
const at = (offsetMs: number) => new Date(FIXTURE_NOW.getTime() + offsetMs);
|
|
96
|
-
|
|
97
|
-
/** The keys an offering carries (D4). */
|
|
98
|
-
const OFFERING_KEYS = [
|
|
99
|
-
"ailkGroup",
|
|
100
|
-
"ailkOrder",
|
|
101
|
-
"description",
|
|
102
|
-
"marketingFeatures",
|
|
103
|
-
"name",
|
|
104
|
-
"note",
|
|
105
|
-
"plan",
|
|
106
|
-
"prices",
|
|
107
|
-
"productId",
|
|
108
|
-
"recommended",
|
|
109
|
-
"termsNote",
|
|
110
|
-
"unitLabel",
|
|
111
|
-
];
|
|
112
|
-
const PRICE_KEYS = ["amount", "currency", "interval", "lookupKey", "period", "plan", "priceId"];
|
|
113
|
-
|
|
114
|
-
function enabledRow(): Row {
|
|
115
|
-
return db.__db.organizationOfferings!.find((r) => r.organizationId === FIXTURE_ORG)!;
|
|
116
|
-
}
|
|
117
|
-
function offeringsRow(workspaceId = FIXTURE_WS): Row {
|
|
118
|
-
return db.__db.workspaceOfferings!.find((r) => r.workspaceId === workspaceId)!;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** A `sourceFor` that answers the fixture catalog and counts each source built. */
|
|
122
|
-
function countingSourceFor(answer: () => Promise<typeof OFFERINGS_FIXTURE_CATALOG> = async () =>
|
|
123
|
-
structuredClone(OFFERINGS_FIXTURE_CATALOG)) {
|
|
124
|
-
const calls: string[] = [];
|
|
125
|
-
const sourceFor: OfferingsSourceFor = (organizationId) => {
|
|
126
|
-
calls.push(organizationId);
|
|
127
|
-
return { listActiveCatalog: answer };
|
|
128
|
-
};
|
|
129
|
-
return { sourceFor, calls };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const rejecting = () =>
|
|
133
|
-
countingSourceFor(async () => {
|
|
134
|
-
throw new Error("upstream failed");
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
beforeEach(async () => {
|
|
138
|
-
for (const key of Object.keys(db.__db)) delete db.__db[key];
|
|
139
|
-
mockLog.mockClear();
|
|
140
|
-
db.workspaceOfferings.findUnique.mockClear();
|
|
141
|
-
resetOfferingsRefreshStateForTests();
|
|
142
|
-
await seedOfferings(prisma, { now: FIXTURE_NOW });
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
describe("the typed shape (AC-3)", () => {
|
|
146
|
-
it("getWorkspaceShownSet answers a value its schema parses unchanged, with D4's keys", async () => {
|
|
147
|
-
const shown = await getWorkspaceShownSet(FIXTURE_WS, { now: FIXTURE_NOW });
|
|
148
|
-
expect(workspaceShownSetSchema.parse(shown)).toEqual(shown);
|
|
149
|
-
expect(shown!.offerings.length).toBeGreaterThan(0);
|
|
150
|
-
for (const offering of shown!.offerings) {
|
|
151
|
-
expect(Object.keys(offering).sort()).toEqual(OFFERING_KEYS);
|
|
152
|
-
for (const price of offering.prices) expect(Object.keys(price).sort()).toEqual(PRICE_KEYS);
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it("getOrgEnabledProducts answers a value its schema parses unchanged, every recommended false", async () => {
|
|
157
|
-
const org = await getOrgEnabledProducts(FIXTURE_ORG, fixtureSourceFor, { now: FIXTURE_NOW });
|
|
158
|
-
expect(orgEnabledProductsSchema.parse(org)).toEqual(org);
|
|
159
|
-
expect(org).toEqual({
|
|
160
|
-
organizationId: FIXTURE_ORG,
|
|
161
|
-
productIds: FIXTURE_ENABLED_PRODUCT_IDS,
|
|
162
|
-
offerings: EXPECTED_FIXTURE_OFFERINGS.filter((o) => FIXTURE_ENABLED_PRODUCT_IDS.includes(o.productId)).map(
|
|
163
|
-
(o) => ({ ...o, recommended: false }),
|
|
164
|
-
),
|
|
165
|
-
held: [],
|
|
166
|
-
});
|
|
167
|
-
for (const offering of org!.offerings) expect(Object.keys(offering).sort()).toEqual(OFFERING_KEYS);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it("getOrgEnabledProducts filters the held list to the enabled set", async () => {
|
|
171
|
-
enabledRow().enabledProductIds = ["prod_fixture_growth", "prod_fixture_legacy"];
|
|
172
|
-
const org = await getOrgEnabledProducts(FIXTURE_ORG, fixtureSourceFor, { now: FIXTURE_NOW });
|
|
173
|
-
expect(org!.offerings.map((o) => o.productId)).toEqual(["prod_fixture_growth"]);
|
|
174
|
-
expect(org!.held).toEqual(EXPECTED_FIXTURE_HELD.filter((h) => h.productId === "prod_fixture_legacy"));
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it.each([
|
|
178
|
-
["getWorkspaceShownSet", workspaceShownSetSchema, () => getWorkspaceShownSet(FIXTURE_WS, { now: FIXTURE_NOW })],
|
|
179
|
-
[
|
|
180
|
-
"getOrgEnabledProducts",
|
|
181
|
-
orgEnabledProductsSchema,
|
|
182
|
-
() => getOrgEnabledProducts(FIXTURE_ORG, fixtureSourceFor, { now: FIXTURE_NOW }),
|
|
183
|
-
],
|
|
184
|
-
] as const)("%s rejects with OfferingsShapeError when its schema's parse throws", async (_name, schema, read) => {
|
|
185
|
-
const spy = jest.spyOn(schema, "parse").mockImplementation(() => {
|
|
186
|
-
throw new Error("Growth prod_fixture_growth For a growing practice");
|
|
187
|
-
});
|
|
188
|
-
try {
|
|
189
|
-
const error = await read().catch((err: unknown) => err);
|
|
190
|
-
expect(error).toBeInstanceOf(OfferingsShapeError);
|
|
191
|
-
const text = JSON.stringify({ ...(error as object), message: (error as Error).message });
|
|
192
|
-
expect(text).not.toMatch(/Growth|prod_fixture|growing/);
|
|
193
|
-
expect((error as Error).cause).toBeUndefined();
|
|
194
|
-
} finally {
|
|
195
|
-
spy.mockRestore();
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
describe("a shown set with no Stripe call (AC-6)", () => {
|
|
201
|
-
it("getWorkspaceShownSet(ws1) deep-equals EXPECTED_SHOWN_SET", async () => {
|
|
202
|
-
expect(await getWorkspaceShownSet(FIXTURE_WS, { now: FIXTURE_NOW })).toEqual(EXPECTED_SHOWN_SET);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it("a product switched off at the Organization leaves the next answer, with no refresh", async () => {
|
|
206
|
-
enabledRow().enabledProductIds = FIXTURE_ENABLED_PRODUCT_IDS.filter((id) => id !== "prod_fixture_seo_audit");
|
|
207
|
-
const shown = await getWorkspaceShownSet(FIXTURE_WS, { now: FIXTURE_NOW });
|
|
208
|
-
expect(shown!.productIds).toEqual(["prod_fixture_growth", "prod_fixture_basic"]);
|
|
209
|
-
expect(shown!.offerings.map((o) => o.productId)).toEqual(["prod_fixture_basic", "prod_fixture_growth"]);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it("a product hidden by the workspace leaves the next answer too", async () => {
|
|
213
|
-
offeringsRow().shownProductIds = ["prod_fixture_basic"];
|
|
214
|
-
offeringsRow().recommendedPlanProductId = null;
|
|
215
|
-
const shown = await getWorkspaceShownSet(FIXTURE_WS, { now: FIXTURE_NOW });
|
|
216
|
-
expect(shown!.offerings.map((o) => o.productId)).toEqual(["prod_fixture_basic"]);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it("a Recommended id that is not shown answers recommended.plan null", async () => {
|
|
220
|
-
enabledRow().enabledProductIds = ["prod_fixture_basic", "prod_fixture_seo_audit"];
|
|
221
|
-
const shown = await getWorkspaceShownSet(FIXTURE_WS, { now: FIXTURE_NOW });
|
|
222
|
-
expect(shown!.recommended).toEqual({ plan: null, addon: null });
|
|
223
|
-
expect(shown!.offerings.every((o) => o.recommended === false)).toBe(true);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("a Recommended plan id naming an add-on is not Recommended", async () => {
|
|
227
|
-
offeringsRow().recommendedPlanProductId = "prod_fixture_seo_audit";
|
|
228
|
-
offeringsRow().recommendedAddonProductId = "prod_fixture_seo_audit";
|
|
229
|
-
const shown = await getWorkspaceShownSet(FIXTURE_WS, { now: FIXTURE_NOW });
|
|
230
|
-
expect(shown!.recommended).toEqual({ plan: null, addon: "prod_fixture_seo_audit" });
|
|
231
|
-
expect(shown!.offerings.filter((o) => o.recommended).map((o) => o.productId)).toEqual([
|
|
232
|
-
"prod_fixture_seo_audit",
|
|
233
|
-
]);
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it("ws2 (same Organization, no Offerings row) answers null", async () => {
|
|
237
|
-
expect(await getWorkspaceShownSet(FIXTURE_WS_2, { now: FIXTURE_NOW })).toBeNull();
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it("a workspace of an Organization with no connection answers null, even with a selection", async () => {
|
|
241
|
-
expect(offeringsRow(FIXTURE_ORG_2_WS)).toBeDefined();
|
|
242
|
-
expect(await getWorkspaceShownSet(FIXTURE_ORG_2_WS, { now: FIXTURE_NOW })).toBeNull();
|
|
243
|
-
expect(await getOrgEnabledProducts(FIXTURE_ORG_2, refusingSourceFor)).toBeNull();
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it("an unreadable connection on a workspace with no Offerings row still serves nothing, never null", async () => {
|
|
247
|
-
db.__db.stripeConnection![0]!.mode = "prod";
|
|
248
|
-
expect(await getSellableProductIds(FIXTURE_WS_2)).toEqual({ organizationId: FIXTURE_ORG, productIds: [], priceIds: null });
|
|
249
|
-
expect(await getWorkspaceShownSet(FIXTURE_WS_2, { now: FIXTURE_NOW })).toMatchObject({
|
|
250
|
-
workspaceId: FIXTURE_WS_2,
|
|
251
|
-
productIds: [],
|
|
252
|
-
offerings: [],
|
|
253
|
-
state: "empty",
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it("an unknown workspace answers null", async () => {
|
|
258
|
-
expect(await getWorkspaceShownSet("wksp_01920000-8876-7000-8000-0000000000ff")).toBeNull();
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it("an unreadable connection serves nothing and never reads as unconnected", async () => {
|
|
262
|
-
db.__db.stripeConnection![0]!.mode = "prod";
|
|
263
|
-
expect(await getWorkspaceShownSet(FIXTURE_WS, { now: FIXTURE_NOW })).toEqual({
|
|
264
|
-
workspaceId: FIXTURE_WS,
|
|
265
|
-
organizationId: FIXTURE_ORG,
|
|
266
|
-
productIds: [],
|
|
267
|
-
recommended: { plan: null, addon: null },
|
|
268
|
-
offerings: [],
|
|
269
|
-
state: "empty",
|
|
270
|
-
snapshotAt: null,
|
|
271
|
-
});
|
|
272
|
-
expect(await getSellableProductIds(FIXTURE_WS)).toEqual({ organizationId: FIXTURE_ORG, productIds: [], priceIds: null });
|
|
273
|
-
expect(await getOrgEnabledProducts(FIXTURE_ORG, refusingSourceFor)).toEqual({
|
|
274
|
-
organizationId: FIXTURE_ORG,
|
|
275
|
-
productIds: FIXTURE_ENABLED_PRODUCT_IDS,
|
|
276
|
-
offerings: [],
|
|
277
|
-
held: [],
|
|
278
|
-
});
|
|
279
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS, refusingSourceFor, { now: at(61 * SECOND) })).toBe(
|
|
280
|
-
"no_connection",
|
|
281
|
-
);
|
|
282
|
-
});
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
describe("the per-Price shown set (#9070 D2, AC-4, AC-5)", () => {
|
|
286
|
-
const prices = (shown: Awaited<ReturnType<typeof getWorkspaceShownSet>>, productId: string) =>
|
|
287
|
-
shown!.offerings.find((o) => o.productId === productId)?.prices.map((p) => p.priceId);
|
|
288
|
-
|
|
289
|
-
it("a row with no stored Prices answers exactly what it did before, and sells at the Product grain", async () => {
|
|
290
|
-
expect(offeringsRow().shownPriceIds).toEqual([]);
|
|
291
|
-
expect(await getWorkspaceShownSet(FIXTURE_WS, { now: FIXTURE_NOW })).toEqual(EXPECTED_SHOWN_SET);
|
|
292
|
-
expect(await getSellableProductIds(FIXTURE_WS)).toEqual({
|
|
293
|
-
organizationId: FIXTURE_ORG,
|
|
294
|
-
productIds: FIXTURE_SHOWN_PRODUCT_IDS,
|
|
295
|
-
priceIds: null,
|
|
296
|
-
});
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it("stored Prices keep only those Prices, drop a Product left with none, and sell only them", async () => {
|
|
300
|
-
offeringsRow().shownProductIds = ["prod_fixture_growth", "prod_fixture_basic"];
|
|
301
|
-
offeringsRow().shownPriceIds = ["price_fixture_growth_yearly", "price_fixture_basic_once"];
|
|
302
|
-
const shown = await getWorkspaceShownSet(FIXTURE_WS, { now: FIXTURE_NOW });
|
|
303
|
-
expect(workspaceShownSetSchema.parse(shown)).toEqual(shown);
|
|
304
|
-
expect(shown!.offerings.map((o) => o.productId)).toEqual(["prod_fixture_basic", "prod_fixture_growth"]);
|
|
305
|
-
expect(prices(shown, "prod_fixture_growth")).toEqual(["price_fixture_growth_yearly"]);
|
|
306
|
-
expect(prices(shown, "prod_fixture_basic")).toEqual(["price_fixture_basic_once"]);
|
|
307
|
-
expect(prices(shown, "prod_fixture_seo_audit")).toBeUndefined();
|
|
308
|
-
expect(await getSellableProductIds(FIXTURE_WS)).toEqual({
|
|
309
|
-
organizationId: FIXTURE_ORG,
|
|
310
|
-
productIds: ["prod_fixture_growth", "prod_fixture_basic"],
|
|
311
|
-
priceIds: ["price_fixture_growth_yearly", "price_fixture_basic_once"],
|
|
312
|
-
});
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
it("keeps the stored Recommended on a Product with a shown Price — the flag /pricing highlights from", async () => {
|
|
316
|
-
offeringsRow().shownProductIds = ["prod_fixture_growth"];
|
|
317
|
-
offeringsRow().shownPriceIds = ["price_fixture_growth_yearly"];
|
|
318
|
-
const shown = await getWorkspaceShownSet(FIXTURE_WS, { now: FIXTURE_NOW });
|
|
319
|
-
expect(shown!.recommended).toEqual({ plan: "prod_fixture_growth", addon: null });
|
|
320
|
-
expect(shown!.offerings).toHaveLength(1);
|
|
321
|
-
expect(shown!.offerings[0]).toMatchObject({ productId: "prod_fixture_growth", recommended: true });
|
|
322
|
-
expect(prices(shown, "prod_fixture_growth")).toEqual(["price_fixture_growth_yearly"]);
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
it("a stored Price the catalog no longer carries is not shown, and its Recommended is cleared", async () => {
|
|
326
|
-
offeringsRow().shownProductIds = ["prod_fixture_growth"];
|
|
327
|
-
offeringsRow().shownPriceIds = ["price_fixture_gone"];
|
|
328
|
-
const shown = await getWorkspaceShownSet(FIXTURE_WS, { now: FIXTURE_NOW });
|
|
329
|
-
expect(shown!.offerings).toEqual([]);
|
|
330
|
-
expect(shown!.recommended.plan).toBeNull();
|
|
331
|
-
expect(shown!.state).toBe("current");
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
it("shownPriceIdsFor answers stored ∩ carried, or every Price of each shown Product with none stored", () => {
|
|
335
|
-
const offerings = EXPECTED_FIXTURE_OFFERINGS.filter((o) => FIXTURE_SHOWN_PRODUCT_IDS.includes(o.productId));
|
|
336
|
-
expect(
|
|
337
|
-
shownPriceIdsFor(offerings, {
|
|
338
|
-
shownProductIds: ["prod_fixture_growth"],
|
|
339
|
-
shownPriceIds: ["price_fixture_growth_yearly", "price_fixture_gone"],
|
|
340
|
-
}),
|
|
341
|
-
).toEqual(new Set(["price_fixture_growth_yearly"]));
|
|
342
|
-
expect(
|
|
343
|
-
shownPriceIdsFor(offerings, { shownProductIds: ["prod_fixture_growth"], shownPriceIds: [] }),
|
|
344
|
-
).toEqual(new Set(["price_fixture_growth_monthly", "price_fixture_growth_yearly"]));
|
|
345
|
-
expect(shownPriceIdsFor(offerings, { shownProductIds: [], shownPriceIds: [] })).toEqual(new Set());
|
|
346
|
-
});
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
describe("the last-good copy, for 24 hours (AC-7)", () => {
|
|
350
|
-
it("is current at snapshotAt + 24h and expired, with no offerings, one millisecond later", async () => {
|
|
351
|
-
const current = await getWorkspaceShownSet(FIXTURE_WS, { now: at(LAST_GOOD_MAX_AGE_MS) });
|
|
352
|
-
expect(current!.state).toBe("current");
|
|
353
|
-
expect(current!.offerings).toHaveLength(3);
|
|
354
|
-
const expired = await getWorkspaceShownSet(FIXTURE_WS, { now: at(LAST_GOOD_MAX_AGE_MS + 1) });
|
|
355
|
-
expect(expired).toMatchObject({
|
|
356
|
-
state: "expired",
|
|
357
|
-
offerings: [],
|
|
358
|
-
recommended: { plan: null, addon: null },
|
|
359
|
-
snapshotAt: FIXTURE_NOW.toISOString(),
|
|
360
|
-
});
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
it("a snapshotAt ten minutes in the future answers expired", async () => {
|
|
364
|
-
offeringsRow().snapshotAt = at(10 * 60 * SECOND);
|
|
365
|
-
expect((await getWorkspaceShownSet(FIXTURE_WS, { now: FIXTURE_NOW }))!.state).toBe("expired");
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
it("a snapshot that fails its schema answers empty and logs snapshot_unreadable", async () => {
|
|
369
|
-
offeringsRow().snapshot = { v: 1, offerings: [{ productId: "prod_fixture_growth", name: "" }] };
|
|
370
|
-
const shown = await getWorkspaceShownSet(FIXTURE_WS, { now: FIXTURE_NOW });
|
|
371
|
-
expect(shown).toMatchObject({ state: "empty", offerings: [], snapshotAt: null });
|
|
372
|
-
expect(mockLog).toHaveBeenCalledWith("warn", { workspaceId: FIXTURE_WS, reason: "snapshot_unreadable" }, expect.any(String));
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
it("no snapshot answers empty", async () => {
|
|
376
|
-
offeringsRow().snapshot = null;
|
|
377
|
-
offeringsRow().snapshotAt = null;
|
|
378
|
-
expect((await getWorkspaceShownSet(FIXTURE_WS, { now: FIXTURE_NOW }))!.state).toBe("empty");
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
it("a refresh whose source rejects answers source_failed and leaves the last good copy byte-equal", async () => {
|
|
382
|
-
const before = JSON.stringify([offeringsRow().snapshot, offeringsRow().snapshotAt]);
|
|
383
|
-
const { sourceFor } = rejecting();
|
|
384
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS, sourceFor, { now: at(61 * SECOND) })).toBe("source_failed");
|
|
385
|
-
expect(JSON.stringify([offeringsRow().snapshot, offeringsRow().snapshotAt])).toBe(before);
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
it("a refresh after 61 seconds answers refreshed and rewrites both columns through the schema", async () => {
|
|
389
|
-
const changed = structuredClone(OFFERINGS_FIXTURE_CATALOG);
|
|
390
|
-
changed.products[0] = { ...changed.products[0], name: "Growth, renamed" };
|
|
391
|
-
const { sourceFor } = countingSourceFor(async () => structuredClone(changed));
|
|
392
|
-
const now = at(61 * SECOND);
|
|
393
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS, sourceFor, { now })).toBe("refreshed");
|
|
394
|
-
expect(offeringsRow().snapshotAt).toEqual(now);
|
|
395
|
-
const snapshot = offeringsSnapshotSchema.parse(offeringsRow().snapshot);
|
|
396
|
-
expect(snapshot.offerings.map((o) => o.name)).toEqual(["Basic", "Growth, renamed", "SEO audit"]);
|
|
397
|
-
expect((await getWorkspaceShownSet(FIXTURE_WS, { now }))!.offerings[1]!.name).toBe("Growth, renamed");
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
it("a refresh writes only the shown ∩ enabled products", async () => {
|
|
401
|
-
offeringsRow().shownProductIds = ["prod_fixture_growth", "prod_fixture_bidi"];
|
|
402
|
-
const { sourceFor } = countingSourceFor();
|
|
403
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS, sourceFor, { now: at(61 * SECOND) })).toBe("refreshed");
|
|
404
|
-
const snapshot = offeringsSnapshotSchema.parse(offeringsRow().snapshot);
|
|
405
|
-
expect(snapshot.offerings.map((o) => o.productId)).toEqual(["prod_fixture_growth"]);
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
it("a refresh of a workspace with no Offerings row answers no_selection and creates none", async () => {
|
|
409
|
-
const { sourceFor, calls } = countingSourceFor();
|
|
410
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS_2, sourceFor, { now: at(61 * SECOND) })).toBe("no_selection");
|
|
411
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_ORG_2_WS, sourceFor, { now: at(61 * SECOND) })).toBe(
|
|
412
|
-
"no_connection",
|
|
413
|
-
);
|
|
414
|
-
expect(calls).toEqual([]);
|
|
415
|
-
expect(db.__db.workspaceOfferings!.some((r) => r.workspaceId === FIXTURE_WS_2)).toBe(false);
|
|
416
|
-
});
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
describe("the refresh is bounded (AC-8, T7)", () => {
|
|
420
|
-
it("59 seconds after snapshotAt answers fresh with no call", async () => {
|
|
421
|
-
const { sourceFor, calls } = countingSourceFor();
|
|
422
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS, sourceFor, { now: at(59 * SECOND) })).toBe("fresh");
|
|
423
|
-
expect(calls).toHaveLength(0);
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
it("five concurrent refreshes after 61 seconds call the source once and all answer refreshed", async () => {
|
|
427
|
-
const { sourceFor, calls } = countingSourceFor();
|
|
428
|
-
const now = at(61 * SECOND);
|
|
429
|
-
const outcomes = await Promise.all(
|
|
430
|
-
Array.from({ length: 5 }, () => refreshWorkspaceSnapshot(FIXTURE_WS, sourceFor, { now, force: true })),
|
|
431
|
-
);
|
|
432
|
-
expect(outcomes).toEqual(Array(5).fill("refreshed"));
|
|
433
|
-
expect(calls).toEqual([FIXTURE_ORG]);
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
it("five concurrent non-forced refreshes after 61 seconds call the source once", async () => {
|
|
437
|
-
const { sourceFor, calls } = countingSourceFor();
|
|
438
|
-
const now = at(61 * SECOND);
|
|
439
|
-
const outcomes = await Promise.all(
|
|
440
|
-
Array.from({ length: 5 }, () => refreshWorkspaceSnapshot(FIXTURE_WS, sourceFor, { now })),
|
|
441
|
-
);
|
|
442
|
-
expect(outcomes).toEqual(Array(5).fill("refreshed"));
|
|
443
|
-
expect(calls).toHaveLength(1);
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
it("after a source_failed, a non-forced refresh 30 seconds later is throttled with no call", async () => {
|
|
447
|
-
const { sourceFor, calls } = rejecting();
|
|
448
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS, sourceFor, { now: at(61 * SECOND) })).toBe("source_failed");
|
|
449
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS, sourceFor, { now: at(91 * SECOND) })).toBe("throttled");
|
|
450
|
-
expect(calls).toHaveLength(1);
|
|
451
|
-
// A minute after the failure the Organization may be read again.
|
|
452
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS, sourceFor, { now: at(121 * SECOND) })).toBe("source_failed");
|
|
453
|
-
expect(calls).toHaveLength(2);
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
it("a second workspace of the Organization 30 seconds after a read is served from it, with no call", async () => {
|
|
457
|
-
await db.workspaceOfferings.create({
|
|
458
|
-
data: {
|
|
459
|
-
workspaceId: FIXTURE_WS_2,
|
|
460
|
-
shownProductIds: ["prod_fixture_seo_audit"],
|
|
461
|
-
recommendedPlanProductId: null,
|
|
462
|
-
recommendedAddonProductId: null,
|
|
463
|
-
snapshot: null,
|
|
464
|
-
snapshotAt: null,
|
|
465
|
-
},
|
|
466
|
-
});
|
|
467
|
-
const { sourceFor, calls } = countingSourceFor();
|
|
468
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS, sourceFor, { now: at(61 * SECOND) })).toBe("refreshed");
|
|
469
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS_2, sourceFor, { now: at(91 * SECOND) })).toBe("refreshed");
|
|
470
|
-
expect(calls).toHaveLength(1);
|
|
471
|
-
expect(offeringsSnapshotSchema.parse(offeringsRow(FIXTURE_WS_2).snapshot).offerings.map((o) => o.productId)).toEqual([
|
|
472
|
-
"prod_fixture_seo_audit",
|
|
473
|
-
]);
|
|
474
|
-
expect(offeringsRow(FIXTURE_WS_2).snapshotAt).toEqual(at(91 * SECOND));
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
it("a refresh never overwrites a snapshot newer than its own now, and answers fresh", async () => {
|
|
478
|
-
offeringsRow().snapshotAt = at(120 * SECOND);
|
|
479
|
-
const before = JSON.stringify(offeringsRow().snapshot);
|
|
480
|
-
const { sourceFor } = countingSourceFor();
|
|
481
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS, sourceFor, { now: at(61 * SECOND), force: true })).toBe(
|
|
482
|
-
"fresh",
|
|
483
|
-
);
|
|
484
|
-
expect(offeringsRow().snapshotAt).toEqual(at(120 * SECOND));
|
|
485
|
-
expect(JSON.stringify(offeringsRow().snapshot)).toBe(before);
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
it("a refresh replaces a snapshot dated past the skew window", async () => {
|
|
489
|
-
offeringsRow().snapshotAt = at(61 * SECOND + 10 * 60 * SECOND);
|
|
490
|
-
const { sourceFor } = countingSourceFor();
|
|
491
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS, sourceFor, { now: at(61 * SECOND) })).toBe("refreshed");
|
|
492
|
-
expect(offeringsRow().snapshotAt).toEqual(at(61 * SECOND));
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
it("a caller whose now is older than the last read is still held by it (out-of-order now)", async () => {
|
|
496
|
-
const failing = rejecting();
|
|
497
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS, failing.sourceFor, { now: at(100 * SECOND) })).toBe(
|
|
498
|
-
"source_failed",
|
|
499
|
-
);
|
|
500
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS, failing.sourceFor, { now: at(100 * SECOND - 5) })).toBe(
|
|
501
|
-
"throttled",
|
|
502
|
-
);
|
|
503
|
-
await expect(
|
|
504
|
-
getOrgEnabledProducts(FIXTURE_ORG, failing.sourceFor, { now: at(100 * SECOND - 10) }),
|
|
505
|
-
).rejects.toBeInstanceOf(OfferingsSourceError);
|
|
506
|
-
expect(failing.calls).toHaveLength(1);
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
it("an older now never evicts another Organization's hold", async () => {
|
|
510
|
-
const failing = rejecting();
|
|
511
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS, failing.sourceFor, { now: at(100 * SECOND) })).toBe(
|
|
512
|
-
"source_failed",
|
|
513
|
-
);
|
|
514
|
-
// A read for another Organization arriving with an older clock.
|
|
515
|
-
await getOrgEnabledProducts(FIXTURE_ORG_2, refusingSourceFor, { now: at(50 * SECOND) });
|
|
516
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS, failing.sourceFor, { now: at(110 * SECOND) })).toBe(
|
|
517
|
-
"throttled",
|
|
518
|
-
);
|
|
519
|
-
expect(failing.calls).toHaveLength(1);
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
it("a read dated more than the skew after now does not hold the Organization", async () => {
|
|
523
|
-
const failing = rejecting();
|
|
524
|
-
await getOrgEnabledProducts(FIXTURE_ORG, failing.sourceFor, { now: at(60 * 60 * SECOND) }).catch(() => undefined);
|
|
525
|
-
await expect(getOrgEnabledProducts(FIXTURE_ORG, failing.sourceFor, { now: FIXTURE_NOW })).rejects.toBeInstanceOf(
|
|
526
|
-
OfferingsSourceError,
|
|
527
|
-
);
|
|
528
|
-
expect(failing.calls).toHaveLength(2);
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
it("a snapshot dated within the skew after now is fresh, with no call", async () => {
|
|
532
|
-
offeringsRow().snapshotAt = at(4 * 60 * SECOND);
|
|
533
|
-
const { sourceFor, calls } = countingSourceFor();
|
|
534
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS, sourceFor, { now: FIXTURE_NOW })).toBe("fresh");
|
|
535
|
-
expect(calls).toHaveLength(0);
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
it("getOrgEnabledProducts 30 seconds after a failed read rejects with OfferingsSourceError and no call", async () => {
|
|
539
|
-
const { sourceFor, calls } = rejecting();
|
|
540
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS, sourceFor, { now: at(61 * SECOND) })).toBe("source_failed");
|
|
541
|
-
await expect(getOrgEnabledProducts(FIXTURE_ORG, sourceFor, { now: at(91 * SECOND) })).rejects.toBeInstanceOf(
|
|
542
|
-
OfferingsSourceError,
|
|
543
|
-
);
|
|
544
|
-
expect(calls).toHaveLength(1);
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
it("getOrgEnabledProducts reuses a read under a minute old, and its own read holds the Organization", async () => {
|
|
548
|
-
const { sourceFor, calls } = countingSourceFor();
|
|
549
|
-
await getOrgEnabledProducts(FIXTURE_ORG, sourceFor, { now: FIXTURE_NOW });
|
|
550
|
-
await getOrgEnabledProducts(FIXTURE_ORG, sourceFor, { now: at(30 * SECOND) });
|
|
551
|
-
expect(calls).toHaveLength(1);
|
|
552
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS, sourceFor, { now: at(61 * SECOND) })).toBe("refreshed");
|
|
553
|
-
expect(calls).toHaveLength(2);
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
it("force: true at 59 seconds calls the source once", async () => {
|
|
557
|
-
const { sourceFor, calls } = countingSourceFor();
|
|
558
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS, sourceFor, { now: at(59 * SECOND), force: true })).toBe(
|
|
559
|
-
"refreshed",
|
|
560
|
-
);
|
|
561
|
-
expect(calls).toHaveLength(1);
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
it("builds a source only for the Organization resolved from Workspace.organizationId", async () => {
|
|
565
|
-
const { sourceFor, calls } = countingSourceFor();
|
|
566
|
-
await refreshWorkspaceSnapshot(FIXTURE_WS, sourceFor, { now: at(61 * SECOND), force: true });
|
|
567
|
-
await getOrgEnabledProducts(FIXTURE_ORG, sourceFor, { now: at(200 * SECOND) });
|
|
568
|
-
expect(new Set(calls)).toEqual(new Set([FIXTURE_ORG]));
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
it("getWorkspaceShownSet and getSellableProductIds never build a source", async () => {
|
|
572
|
-
const { sourceFor, calls } = countingSourceFor();
|
|
573
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS, sourceFor, { now: at(61 * SECOND) })).toBe("refreshed");
|
|
574
|
-
for (const offset of [0, 61 * SECOND, LAST_GOOD_MAX_AGE_MS + 1]) {
|
|
575
|
-
await getWorkspaceShownSet(FIXTURE_WS, { now: at(offset) });
|
|
576
|
-
await getSellableProductIds(FIXTURE_WS);
|
|
577
|
-
}
|
|
578
|
-
expect(calls).toHaveLength(1);
|
|
579
|
-
});
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
describe("checkout's allowlist never reads the display copy (AC-10, contract A4)", () => {
|
|
583
|
-
it("answers the seeded enabled ∩ shown ids, never selecting the snapshot", async () => {
|
|
584
|
-
expect(await getSellableProductIds(FIXTURE_WS)).toEqual({
|
|
585
|
-
organizationId: FIXTURE_ORG,
|
|
586
|
-
productIds: FIXTURE_SHOWN_PRODUCT_IDS,
|
|
587
|
-
priceIds: null,
|
|
588
|
-
});
|
|
589
|
-
expect(db.workspaceOfferings.findUnique).toHaveBeenCalled();
|
|
590
|
-
for (const [args] of db.workspaceOfferings.findUnique.mock.calls as Array<[{ select: Row }]>) {
|
|
591
|
-
expect(Object.keys(args.select)).not.toContain("snapshot");
|
|
592
|
-
expect(Object.keys(args.select)).not.toContain("snapshotAt");
|
|
593
|
-
}
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
it("is unchanged when the snapshot names a product neither set holds", async () => {
|
|
597
|
-
const before = await getSellableProductIds(FIXTURE_WS);
|
|
598
|
-
offeringsRow().snapshot = {
|
|
599
|
-
v: 1,
|
|
600
|
-
offerings: [{ ...EXPECTED_FIXTURE_OFFERINGS[3], productId: "prod_fixture_bidi" }],
|
|
601
|
-
};
|
|
602
|
-
expect(await getSellableProductIds(FIXTURE_WS)).toEqual(before);
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
it("answers null with no connection, and for a workspace with no Offerings row", async () => {
|
|
606
|
-
expect(await getSellableProductIds(FIXTURE_ORG_2_WS)).toBeNull();
|
|
607
|
-
expect(await getSellableProductIds(FIXTURE_WS_2)).toBeNull();
|
|
608
|
-
});
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
describe("nothing leaks into logs or errors (AC-12, contract C5, T6)", () => {
|
|
612
|
-
/** Every string the fixture holds that must never be logged. */
|
|
613
|
-
function fixtureSecrets(): string[] {
|
|
614
|
-
const texts: string[] = [FIXTURE_KEY_LAST4, FIXTURE_WEBHOOK_TOKEN, FIXTURE_CIPHERTEXT_PLACEHOLDER];
|
|
615
|
-
for (const p of OFFERINGS_FIXTURE_CATALOG.products) {
|
|
616
|
-
texts.push(p.id as string, p.name as string);
|
|
617
|
-
if (typeof p.description === "string") texts.push(p.description);
|
|
618
|
-
for (const f of (p.marketing_features as Array<{ name: string }>) ?? []) texts.push(f.name);
|
|
619
|
-
}
|
|
620
|
-
for (const price of OFFERINGS_FIXTURE_CATALOG.prices) texts.push(price.id as string, price.lookup_key as string);
|
|
621
|
-
return texts;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
it("a log capture across every reader and every refresh outcome holds no fixture text", async () => {
|
|
625
|
-
const key = `rk_test_${"Zx9".repeat(12)}`;
|
|
626
|
-
const keyed = countingSourceFor(async () => {
|
|
627
|
-
throw Object.assign(new Error(`Invalid API Key provided: ${key}`), { raw: { message: key } });
|
|
628
|
-
});
|
|
629
|
-
const { sourceFor } = countingSourceFor();
|
|
630
|
-
|
|
631
|
-
await getWorkspaceShownSet(FIXTURE_WS, { now: FIXTURE_NOW });
|
|
632
|
-
await getSellableProductIds(FIXTURE_WS);
|
|
633
|
-
await getOrgEnabledProducts(FIXTURE_ORG, sourceFor, { now: FIXTURE_NOW });
|
|
634
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS, sourceFor, { now: at(30 * SECOND) })).toBe("fresh");
|
|
635
|
-
resetOfferingsRefreshStateForTests();
|
|
636
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS, keyed.sourceFor, { now: at(61 * SECOND) })).toBe(
|
|
637
|
-
"source_failed",
|
|
638
|
-
);
|
|
639
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS, keyed.sourceFor, { now: at(62 * SECOND) })).toBe("throttled");
|
|
640
|
-
const sourceError = await getOrgEnabledProducts(FIXTURE_ORG, keyed.sourceFor, { now: at(63 * SECOND) }).catch(
|
|
641
|
-
(err: unknown) => err,
|
|
642
|
-
);
|
|
643
|
-
resetOfferingsRefreshStateForTests();
|
|
644
|
-
const orgError = await getOrgEnabledProducts(FIXTURE_ORG, keyed.sourceFor, { now: at(64 * SECOND) }).catch(
|
|
645
|
-
(err: unknown) => err,
|
|
646
|
-
);
|
|
647
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS, sourceFor, { now: at(200 * SECOND) })).toBe("refreshed");
|
|
648
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_WS_2, sourceFor, { now: at(200 * SECOND) })).toBe("no_selection");
|
|
649
|
-
expect(await refreshWorkspaceSnapshot(FIXTURE_ORG_2_WS, sourceFor, { now: at(200 * SECOND) })).toBe(
|
|
650
|
-
"no_connection",
|
|
651
|
-
);
|
|
652
|
-
offeringsRow().snapshot = { v: 1, offerings: [{ productId: "prod_fixture_growth", name: "Growth" }] };
|
|
653
|
-
await getWorkspaceShownSet(FIXTURE_WS, { now: at(200 * SECOND) });
|
|
654
|
-
db.__db.stripeConnection![0]!.keyLast4 = "42";
|
|
655
|
-
await getWorkspaceShownSet(FIXTURE_WS, { now: at(200 * SECOND) });
|
|
656
|
-
|
|
657
|
-
expect(mockLog).toHaveBeenCalled();
|
|
658
|
-
const logged = JSON.stringify(mockLog.mock.calls);
|
|
659
|
-
for (const secret of [...fixtureSecrets(), key, "Invalid API Key", "upstream"]) {
|
|
660
|
-
expect(logged).not.toContain(secret);
|
|
661
|
-
}
|
|
662
|
-
expect(mockLog).toHaveBeenCalledWith(
|
|
663
|
-
"warn",
|
|
664
|
-
{ workspaceId: FIXTURE_WS, outcome: "source_failed", errName: "Error" },
|
|
665
|
-
expect.any(String),
|
|
666
|
-
);
|
|
667
|
-
expect(mockLog).toHaveBeenCalledWith(
|
|
668
|
-
"warn",
|
|
669
|
-
{ organizationId: FIXTURE_ORG, outcome: "source_failed", errName: "Error" },
|
|
670
|
-
expect.any(String),
|
|
671
|
-
);
|
|
672
|
-
|
|
673
|
-
for (const err of [sourceError, orgError]) {
|
|
674
|
-
expect(err).toBeInstanceOf(OfferingsSourceError);
|
|
675
|
-
expect((err as Error).cause).toBeUndefined();
|
|
676
|
-
const text = JSON.stringify({ ...(err as object), message: (err as Error).message, stack: (err as Error).stack });
|
|
677
|
-
expect(text).not.toContain(key);
|
|
678
|
-
expect(text).not.toContain("Invalid API Key");
|
|
679
|
-
}
|
|
680
|
-
});
|
|
681
|
-
});
|