create-ailk 0.1.0
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/LICENSE +201 -0
- package/README.md +39 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +80 -0
- package/dist/copy.d.ts +8 -0
- package/dist/copy.js +109 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/lib/pro-callout.d.ts +12 -0
- package/dist/lib/pro-callout.js +20 -0
- package/dist/parse-args.d.ts +13 -0
- package/dist/parse-args.js +81 -0
- package/dist/programmatic.d.ts +49 -0
- package/dist/programmatic.js +89 -0
- package/dist/substitute.d.ts +31 -0
- package/dist/substitute.js +45 -0
- package/dist/validate.d.ts +8 -0
- package/dist/validate.js +70 -0
- package/package.json +47 -0
- package/templates/.claude/.gitkeep +0 -0
- package/templates/.claude/agents/web.md +80 -0
- package/templates/.claude/rules/architecture.md +62 -0
- package/templates/.claude/rules/preserve-existing-style.md +51 -0
- package/templates/.claude/skills/README.md +43 -0
- package/templates/.claude/skills/add-schema/SKILL.md +270 -0
- package/templates/.claude/skills/add-schema/references/conflict-handling.md +200 -0
- package/templates/.claude/skills/add-schema/references/file-finding.md +123 -0
- package/templates/.claude/skills/add-schema/references/schema-types.md +131 -0
- package/templates/.claude/skills/audit-site/SKILL.md +213 -0
- package/templates/.claude/skills/audit-site/references/common-errors.md +113 -0
- package/templates/.claude/skills/audit-site/references/findings-glossary.md +138 -0
- package/templates/.claude/skills/audit-site/references/severity-levels.md +114 -0
- package/templates/.claude/skills/provision-config/SKILL.md +243 -0
- package/templates/.claude/skills/provision-config/references/env-vars-template.md +184 -0
- package/templates/.claude/skills/provision-config/references/runbook-template.md +248 -0
- package/templates/.claude/skills/provision-config/references/vercel-json-template.md +98 -0
- package/templates/.claude/skills/scaffold-auth/SKILL.md +261 -0
- package/templates/.claude/skills/scaffold-auth/references/env-vars.md +91 -0
- package/templates/.claude/skills/scaffold-auth/references/oauth-app-setup.md +171 -0
- package/templates/.claude/skills/scaffold-commerce/SKILL.md +310 -0
- package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +71 -0
- package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +83 -0
- package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +72 -0
- package/templates/.claude/skills/scaffold-page/SKILL.md +243 -0
- package/templates/.claude/skills/scaffold-page/references/page-types.md +169 -0
- package/templates/.claude/skills/scaffold-page/templates/content.mdx.tmpl +28 -0
- package/templates/.claude/skills/scaffold-page/templates/route.tsx.tmpl +28 -0
- package/templates/.claude/skills/suggest-site-pages/SKILL.md +242 -0
- package/templates/.claude/skills/suggest-site-pages/references/categories.md +287 -0
- package/templates/.env.example +205 -0
- package/templates/.gitkeep +2 -0
- package/templates/CLAUDE.md +142 -0
- package/templates/CONVENTIONS.md +222 -0
- package/templates/README.md +110 -0
- package/templates/apps/api/.env.example +40 -0
- package/templates/apps/api/CLAUDE.md +63 -0
- package/templates/apps/api/api/index.ts +144 -0
- package/templates/apps/api/jest.config.cjs +74 -0
- package/templates/apps/api/package.json +54 -0
- package/templates/apps/api/src/__tests__/factories/content-fixture.ts +138 -0
- package/templates/apps/api/src/__tests__/factories/index.ts +70 -0
- package/templates/apps/api/src/__tests__/factories/lead-fixture.ts +82 -0
- package/templates/apps/api/src/__tests__/index.test.ts +118 -0
- package/templates/apps/api/src/__tests__/server.test.ts +207 -0
- package/templates/apps/api/src/__tests__/services/enrichment-shim.test.ts +126 -0
- package/templates/apps/api/src/__tests__/setup.ts +24 -0
- package/templates/apps/api/src/cli.ts +30 -0
- package/templates/apps/api/src/config/__tests__/index.test.ts +97 -0
- package/templates/apps/api/src/config/index.ts +56 -0
- package/templates/apps/api/src/index.ts +32 -0
- package/templates/apps/api/src/lib/__mocks__/prisma-client.js +15 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +45 -0
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +178 -0
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +100 -0
- package/templates/apps/api/src/lib/__tests__/errors.test.ts +94 -0
- package/templates/apps/api/src/lib/__tests__/file-fallback.test.ts +144 -0
- package/templates/apps/api/src/lib/__tests__/fixtures/HomePage/home.mdx +8 -0
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +227 -0
- package/templates/apps/api/src/lib/__tests__/outbound-http.test.ts +125 -0
- package/templates/apps/api/src/lib/__tests__/prisma.test.ts +165 -0
- package/templates/apps/api/src/lib/__tests__/require-paid-tier.test.ts +129 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +74 -0
- package/templates/apps/api/src/lib/content-adapter.ts +86 -0
- package/templates/apps/api/src/lib/error-serializer.ts +77 -0
- package/templates/apps/api/src/lib/errors.ts +95 -0
- package/templates/apps/api/src/lib/file-fallback.ts +87 -0
- package/templates/apps/api/src/lib/lead-capture-store.ts +110 -0
- package/templates/apps/api/src/lib/locales.ts +201 -0
- package/templates/apps/api/src/lib/outbound-http.ts +122 -0
- package/templates/apps/api/src/lib/prisma.ts +21 -0
- package/templates/apps/api/src/lib/require-paid-tier.ts +53 -0
- package/templates/apps/api/src/lib/stripe.ts +30 -0
- package/templates/apps/api/src/lib/webhook-idempotency.ts +92 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +111 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +172 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +119 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +138 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +149 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +139 -0
- package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +175 -0
- package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +76 -0
- package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +153 -0
- package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +72 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +292 -0
- package/templates/apps/api/src/middleware/auth.ts +246 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +132 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +129 -0
- package/templates/apps/api/src/openapi/index.ts +12 -0
- package/templates/apps/api/src/openapi/spec.ts +408 -0
- package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +193 -0
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +201 -0
- package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +294 -0
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +486 -0
- package/templates/apps/api/src/routes/auth/tokens.ts +199 -0
- package/templates/apps/api/src/routes/auth/user.ts +244 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +588 -0
- package/templates/apps/api/src/routes/checkout/index.ts +13 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +185 -0
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +239 -0
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +201 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +141 -0
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +122 -0
- package/templates/apps/api/src/routes/content/__tests__/list-types.test.ts +44 -0
- package/templates/apps/api/src/routes/content/__tests__/locale.test.ts +118 -0
- package/templates/apps/api/src/routes/content/__tests__/rate-limit.test.ts +51 -0
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +252 -0
- package/templates/apps/api/src/routes/content/create.ts +114 -0
- package/templates/apps/api/src/routes/content/delete.ts +81 -0
- package/templates/apps/api/src/routes/content/get-page.ts +98 -0
- package/templates/apps/api/src/routes/content/index.ts +32 -0
- package/templates/apps/api/src/routes/content/list-pages.ts +83 -0
- package/templates/apps/api/src/routes/content/list-types.ts +19 -0
- package/templates/apps/api/src/routes/content/rate-limit.ts +13 -0
- package/templates/apps/api/src/routes/content/slug.ts +9 -0
- package/templates/apps/api/src/routes/content/update.ts +111 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +415 -0
- package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +165 -0
- package/templates/apps/api/src/routes/leads/index.ts +130 -0
- package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +296 -0
- package/templates/apps/api/src/routes/test/auth/session.ts +141 -0
- package/templates/apps/api/src/routes/webhooks/README.md +57 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +247 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +198 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +633 -0
- package/templates/apps/api/src/routes/webhooks/index.ts +41 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +253 -0
- package/templates/apps/api/src/server.ts +157 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +395 -0
- package/templates/apps/api/src/services/enrichment-shim.ts +53 -0
- package/templates/apps/api/src/services/lead-routing.ts +338 -0
- package/templates/apps/api/tsconfig.json +34 -0
- package/templates/apps/api/vercel.json +17 -0
- package/templates/apps/mcp/.env.example +52 -0
- package/templates/apps/mcp/CLAUDE.md +84 -0
- package/templates/apps/mcp/__tests__/abuse/daily-cap.test.ts +52 -0
- package/templates/apps/mcp/__tests__/abuse/email-validator.test.ts +46 -0
- package/templates/apps/mcp/__tests__/abuse/honeypot.test.ts +36 -0
- package/templates/apps/mcp/__tests__/abuse/user-agent-deny.test.ts +54 -0
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +110 -0
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +213 -0
- package/templates/apps/mcp/__tests__/audit.test.ts +217 -0
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +52 -0
- package/templates/apps/mcp/__tests__/create_page.test.ts +201 -0
- package/templates/apps/mcp/__tests__/delete_page.test.ts +176 -0
- package/templates/apps/mcp/__tests__/get_business_hours.test.ts +82 -0
- package/templates/apps/mcp/__tests__/get_faq.test.ts +80 -0
- package/templates/apps/mcp/__tests__/get_locations.test.ts +65 -0
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +90 -0
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +80 -0
- package/templates/apps/mcp/__tests__/get_site_info.test.ts +81 -0
- package/templates/apps/mcp/__tests__/http-transport.test.ts +323 -0
- package/templates/apps/mcp/__tests__/lib/errors.test.ts +75 -0
- package/templates/apps/mcp/__tests__/lib/require-paid-tier.test.ts +75 -0
- package/templates/apps/mcp/__tests__/list_pages.test.ts +112 -0
- package/templates/apps/mcp/__tests__/locale.test.ts +135 -0
- package/templates/apps/mcp/__tests__/parity.test.ts +205 -0
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +34 -0
- package/templates/apps/mcp/__tests__/read_content.test.ts +153 -0
- package/templates/apps/mcp/__tests__/request_callback.test.ts +163 -0
- package/templates/apps/mcp/__tests__/search_content.test.ts +153 -0
- package/templates/apps/mcp/__tests__/server.test.ts +115 -0
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +165 -0
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +161 -0
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +227 -0
- package/templates/apps/mcp/__tests__/tools/scaffold.test.ts +237 -0
- package/templates/apps/mcp/__tests__/update_page.test.ts +195 -0
- package/templates/apps/mcp/jest.config.cjs +82 -0
- package/templates/apps/mcp/package.json +37 -0
- package/templates/apps/mcp/src/abuse/daily-cap.ts +47 -0
- package/templates/apps/mcp/src/abuse/email-validator.ts +25 -0
- package/templates/apps/mcp/src/abuse/honeypot.ts +21 -0
- package/templates/apps/mcp/src/abuse/index.ts +111 -0
- package/templates/apps/mcp/src/abuse/user-agent-deny.ts +58 -0
- package/templates/apps/mcp/src/http-transport.ts +303 -0
- package/templates/apps/mcp/src/index.ts +67 -0
- package/templates/apps/mcp/src/lib/audit-single-page.ts +194 -0
- package/templates/apps/mcp/src/lib/errors.ts +61 -0
- package/templates/apps/mcp/src/lib/locales.ts +171 -0
- package/templates/apps/mcp/src/lib/require-paid-tier.ts +44 -0
- package/templates/apps/mcp/src/lib/resolve-locale.ts +30 -0
- package/templates/apps/mcp/src/server.ts +139 -0
- package/templates/apps/mcp/src/tools/audit.ts +103 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +52 -0
- package/templates/apps/mcp/src/tools/create_page.ts +95 -0
- package/templates/apps/mcp/src/tools/delete_page.ts +73 -0
- package/templates/apps/mcp/src/tools/get_business_hours.ts +66 -0
- package/templates/apps/mcp/src/tools/get_faq.ts +69 -0
- package/templates/apps/mcp/src/tools/get_locations.ts +85 -0
- package/templates/apps/mcp/src/tools/get_offerings.ts +66 -0
- package/templates/apps/mcp/src/tools/get_pricing.ts +74 -0
- package/templates/apps/mcp/src/tools/get_site_info.ts +84 -0
- package/templates/apps/mcp/src/tools/index.ts +125 -0
- package/templates/apps/mcp/src/tools/list_pages.ts +72 -0
- package/templates/apps/mcp/src/tools/purchase.ts +89 -0
- package/templates/apps/mcp/src/tools/read_content.ts +61 -0
- package/templates/apps/mcp/src/tools/request_callback.ts +51 -0
- package/templates/apps/mcp/src/tools/scaffold.ts +119 -0
- package/templates/apps/mcp/src/tools/search_content.ts +59 -0
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +47 -0
- package/templates/apps/mcp/src/tools/update_page.ts +91 -0
- package/templates/apps/mcp/tsconfig.json +19 -0
- package/templates/apps/web/.env.example +47 -0
- package/templates/apps/web/CLAUDE.md +81 -0
- package/templates/apps/web/__tests__/auth/adversarial/cookie-attributes.test.ts +94 -0
- package/templates/apps/web/__tests__/auth/adversarial/oauth-state-parameter.test.ts +75 -0
- package/templates/apps/web/__tests__/auth/adversarial/redirect-uri-allowlist.test.ts +82 -0
- package/templates/apps/web/__tests__/auth/env-helpers.test.ts +168 -0
- package/templates/apps/web/__tests__/authority-list-routes.test.tsx +208 -0
- package/templates/apps/web/__tests__/better-stack.test.ts +66 -0
- package/templates/apps/web/__tests__/blog-dir-resolution.test.ts +87 -0
- package/templates/apps/web/__tests__/blog-source-populated.test.ts +130 -0
- package/templates/apps/web/__tests__/data-testid-coverage.test.tsx +60 -0
- package/templates/apps/web/__tests__/google-analytics.test.tsx +104 -0
- package/templates/apps/web/__tests__/i18n/middleware.test.ts +119 -0
- package/templates/apps/web/__tests__/i18n/routing.test.ts +114 -0
- package/templates/apps/web/__tests__/i18n/translations.test.ts +191 -0
- package/templates/apps/web/__tests__/json-ld.test.ts +98 -0
- package/templates/apps/web/__tests__/knowledge-events-routes.test.tsx +580 -0
- package/templates/apps/web/__tests__/layout-stealth.test.ts +39 -0
- package/templates/apps/web/__tests__/lighthouse-fixtures/improvement-image-shrink.tsx +103 -0
- package/templates/apps/web/__tests__/lighthouse-fixtures/override-fixture.md +114 -0
- package/templates/apps/web/__tests__/lighthouse-fixtures/regression-bloat.tsx +94 -0
- package/templates/apps/web/__tests__/page.test.tsx +230 -0
- package/templates/apps/web/__tests__/products-services-phase1.test.tsx +321 -0
- package/templates/apps/web/__tests__/projects-routes.test.tsx +392 -0
- package/templates/apps/web/__tests__/resources-downloads-integrations-phase2.test.tsx +475 -0
- package/templates/apps/web/__tests__/routes/ai-plugin-json.test.ts +53 -0
- package/templates/apps/web/__tests__/routes/blog-route.test.tsx +191 -0
- package/templates/apps/web/__tests__/routes/llms-txt.test.ts +123 -0
- package/templates/apps/web/__tests__/routes/robots-txt.test.ts +52 -0
- package/templates/apps/web/__tests__/routes/sitemap-xml.test.ts +78 -0
- package/templates/apps/web/__tests__/routes/well-known-mcp.test.ts +203 -0
- package/templates/apps/web/__tests__/section-routes.test.tsx +693 -0
- package/templates/apps/web/__tests__/security-headers.test.ts +201 -0
- package/templates/apps/web/__tests__/sentry-config.test.ts +116 -0
- package/templates/apps/web/__tests__/setup.ts +82 -0
- package/templates/apps/web/__tests__/sitemap-locales.test.ts +415 -0
- package/templates/apps/web/__tests__/standalone-informational-routes.test.tsx +844 -0
- package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +81 -0
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +110 -0
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +58 -0
- package/templates/apps/web/app/[locale]/__tests__/layout.test.tsx +120 -0
- package/templates/apps/web/app/[locale]/__tests__/page-renderer-migration.test.tsx +202 -0
- package/templates/apps/web/app/[locale]/about/page.tsx +31 -0
- package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +173 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +45 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +117 -0
- package/templates/apps/web/app/[locale]/careers/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +113 -0
- package/templates/apps/web/app/[locale]/case-studies/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/clients/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +130 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +152 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +92 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +79 -0
- package/templates/apps/web/app/[locale]/contact/error.tsx +24 -0
- package/templates/apps/web/app/[locale]/contact/page.tsx +38 -0
- package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +119 -0
- package/templates/apps/web/app/[locale]/courses/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +160 -0
- package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +25 -0
- package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +117 -0
- package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +58 -0
- package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +118 -0
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +72 -0
- package/templates/apps/web/app/[locale]/downloads/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +47 -0
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +130 -0
- package/templates/apps/web/app/[locale]/events/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/faq/page.tsx +36 -0
- package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +78 -0
- package/templates/apps/web/app/[locale]/how-to/page.tsx +40 -0
- package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +43 -0
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +129 -0
- package/templates/apps/web/app/[locale]/integrations/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/launch-service/page.tsx +31 -0
- package/templates/apps/web/app/[locale]/layout.tsx +241 -0
- package/templates/apps/web/app/[locale]/page.tsx +41 -0
- package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +113 -0
- package/templates/apps/web/app/[locale]/partners/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +43 -0
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +129 -0
- package/templates/apps/web/app/[locale]/products/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +39 -0
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +138 -0
- package/templates/apps/web/app/[locale]/projects/page.tsx +30 -0
- package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +51 -0
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +117 -0
- package/templates/apps/web/app/[locale]/resources/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +43 -0
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +129 -0
- package/templates/apps/web/app/[locale]/services/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +81 -0
- package/templates/apps/web/app/[locale]/software/page.tsx +39 -0
- package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +82 -0
- package/templates/apps/web/app/[locale]/tech-articles/page.tsx +39 -0
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/testimonials/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/use-cases/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +82 -0
- package/templates/apps/web/app/[locale]/whitepapers/page.tsx +39 -0
- package/templates/apps/web/app/__tests__/error-boundaries.test.tsx +152 -0
- package/templates/apps/web/app/api/auth/[...all]/route.ts +16 -0
- package/templates/apps/web/app/error-fallback.tsx +75 -0
- package/templates/apps/web/app/error.tsx +23 -0
- package/templates/apps/web/app/layout.tsx +18 -0
- package/templates/apps/web/app/llms.txt/route.ts +101 -0
- package/templates/apps/web/app/not-found.tsx +48 -0
- package/templates/apps/web/app/og/__tests__/route.test.ts +164 -0
- package/templates/apps/web/app/og/route.tsx +135 -0
- package/templates/apps/web/app/robots.txt/route.ts +36 -0
- package/templates/apps/web/app/sitemap.xml/route.ts +146 -0
- package/templates/apps/web/auth.ts +141 -0
- package/templates/apps/web/components/ThemeToggle.tsx +60 -0
- package/templates/apps/web/components/google-analytics.tsx +34 -0
- package/templates/apps/web/content/en/blog/index.mdx +15 -0
- package/templates/apps/web/content/en/blog/launching-ailk.mdx +20 -0
- package/templates/apps/web/content/en/docs/getting-started.mdx +30 -0
- package/templates/apps/web/i18n/request.ts +36 -0
- package/templates/apps/web/jest.config.cjs +100 -0
- package/templates/apps/web/lib/__mocks__/next-mdx-remote-rsc.js +12 -0
- package/templates/apps/web/lib/__mocks__/source-server.js +31 -0
- package/templates/apps/web/lib/__tests__/og.test.ts +65 -0
- package/templates/apps/web/lib/auth-env.ts +66 -0
- package/templates/apps/web/lib/blog-list-data.ts +95 -0
- package/templates/apps/web/lib/blog-post-data.ts +79 -0
- package/templates/apps/web/lib/nav-config.ts +13 -0
- package/templates/apps/web/lib/observability/better-stack.ts +27 -0
- package/templates/apps/web/lib/og.ts +23 -0
- package/templates/apps/web/lib/prisma-for-auth.ts +34 -0
- package/templates/apps/web/lib/source.ts +34 -0
- package/templates/apps/web/messages/en.json +131 -0
- package/templates/apps/web/middleware.ts +80 -0
- package/templates/apps/web/next.config.mjs +167 -0
- package/templates/apps/web/package.json +62 -0
- package/templates/apps/web/public/.well-known/security.txt +17 -0
- package/templates/apps/web/public/android-chrome-192x192.png +0 -0
- package/templates/apps/web/public/android-chrome-512x512.png +0 -0
- package/templates/apps/web/public/apple-touch-icon.png +0 -0
- package/templates/apps/web/public/favicon-16x16.png +0 -0
- package/templates/apps/web/public/favicon-32x32.png +0 -0
- package/templates/apps/web/public/favicon.ico +0 -0
- package/templates/apps/web/public/favicon.svg +4 -0
- package/templates/apps/web/public/logo-dark.svg +21 -0
- package/templates/apps/web/public/logo-light.svg +21 -0
- package/templates/apps/web/public/logomark-dark.svg +4 -0
- package/templates/apps/web/public/logomark-light.svg +4 -0
- package/templates/apps/web/public/og-default.png +0 -0
- package/templates/apps/web/public/site.webmanifest +19 -0
- package/templates/apps/web/scripts/assert-content-bundled.ts +55 -0
- package/templates/apps/web/security-headers.config.ts +132 -0
- package/templates/apps/web/sentry.client.config.ts +27 -0
- package/templates/apps/web/sentry.edge.config.ts +15 -0
- package/templates/apps/web/sentry.server.config.ts +20 -0
- package/templates/apps/web/source.config.ts +78 -0
- package/templates/apps/web/src/components/SiteNav.tsx +21 -0
- package/templates/apps/web/src/lib/routes.ts +37 -0
- package/templates/apps/web/tsconfig.json +35 -0
- package/templates/apps/web/types/messages.d.ts +19 -0
- package/templates/content/.gitkeep +0 -0
- package/templates/content/CLAUDE.md +85 -0
- package/templates/content/_site.mdx +38 -0
- package/templates/content/ar/HomePage/home.mdx +123 -0
- package/templates/content/ar/_site.mdx +35 -0
- package/templates/content/de/_site.mdx +29 -0
- package/templates/content/en/.gitkeep +0 -0
- package/templates/content/en/AboutPage/about.mdx +19 -0
- package/templates/content/en/AboutPage/about.schema.json +82 -0
- package/templates/content/en/BookCall/book-call.mdx +19 -0
- package/templates/content/en/BookCall/book-call.schema.json +121 -0
- package/templates/content/en/BreadcrumbList/site-breadcrumbs.mdx +20 -0
- package/templates/content/en/CareersList/careers.mdx +24 -0
- package/templates/content/en/CaseStudiesList/case-studies.mdx +22 -0
- package/templates/content/en/CaseStudiesList/showcase.mdx +8 -0
- package/templates/content/en/CaseStudy/ailk-aeo-rollout.mdx +32 -0
- package/templates/content/en/CaseStudy/schema-builder-migration.mdx +41 -0
- package/templates/content/en/ClientsList/clients.mdx +38 -0
- package/templates/content/en/ContactPage/contact.mdx +19 -0
- package/templates/content/en/ContactPage/contact.schema.json +100 -0
- package/templates/content/en/ContactPoint/primary.mdx +14 -0
- package/templates/content/en/Course/aeo-fundamentals.mdx +37 -0
- package/templates/content/en/Course/schema-builder-workshop.mdx +62 -0
- package/templates/content/en/CoursesList/courses.mdx +22 -0
- package/templates/content/en/DownloadsList/downloads.mdx +22 -0
- package/templates/content/en/Event/aeo-office-hours-may-2026.mdx +39 -0
- package/templates/content/en/Event/ailk-launch-day-2026.mdx +35 -0
- package/templates/content/en/EventsList/events.mdx +26 -0
- package/templates/content/en/FAQPage/faq.mdx +420 -0
- package/templates/content/en/FAQPage/faq.schema.json +231 -0
- package/templates/content/en/GlossaryList/glossary.mdx +19 -0
- package/templates/content/en/GlossaryList/glossary.schema.json +300 -0
- package/templates/content/en/HomePage/home.mdx +312 -0
- package/templates/content/en/HomePage/home.schema.json +88 -0
- package/templates/content/en/HomePage/launch-service.mdx +475 -0
- package/templates/content/en/HomePage/launch-service.schema.json +138 -0
- package/templates/content/en/HowTo/add-mdx-content.mdx +38 -0
- package/templates/content/en/HowTo/configure-aeo-scoring.mdx +35 -0
- package/templates/content/en/HowTo/set-up-json-ld.mdx +36 -0
- package/templates/content/en/Integration/github-actions.mdx +31 -0
- package/templates/content/en/Integration/openai-api.mdx +26 -0
- package/templates/content/en/Integration/vercel.mdx +32 -0
- package/templates/content/en/IntegrationsList/integrations.mdx +22 -0
- package/templates/content/en/JobPosting/content-strategist.mdx +46 -0
- package/templates/content/en/JobPosting/developer-advocate.mdx +47 -0
- package/templates/content/en/JobPosting/senior-fullstack-engineer.mdx +57 -0
- package/templates/content/en/LandingPage/aeo-audit.mdx +19 -0
- package/templates/content/en/LandingPage/aeo-audit.schema.json +92 -0
- package/templates/content/en/LandingPage/agency-waitlist.mdx +340 -0
- package/templates/content/en/LandingPage/agency-waitlist.schema.json +152 -0
- package/templates/content/en/LandingPage/developers.mdx +19 -0
- package/templates/content/en/LandingPage/developers.schema.json +109 -0
- package/templates/content/en/LandingPage/for-agencies.mdx +390 -0
- package/templates/content/en/LandingPage/for-agencies.schema.json +107 -0
- package/templates/content/en/LandingPage/for-ai-search-agencies.mdx +417 -0
- package/templates/content/en/LandingPage/for-ai-search-agencies.schema.json +105 -0
- package/templates/content/en/LandingPage/for-businesses.mdx +19 -0
- package/templates/content/en/LandingPage/for-businesses.schema.json +96 -0
- package/templates/content/en/LandingPage/platform.mdx +369 -0
- package/templates/content/en/LandingPage/platform.schema.json +166 -0
- package/templates/content/en/LandingPage/vs-ai-website-builders.mdx +19 -0
- package/templates/content/en/LandingPage/vs-ai-website-builders.schema.json +102 -0
- package/templates/content/en/LandingPage/vs-wordpress.mdx +19 -0
- package/templates/content/en/LandingPage/vs-wordpress.schema.json +53 -0
- package/templates/content/en/NewsletterSignup/newsletter.mdx +236 -0
- package/templates/content/en/NewsletterSignup/newsletter.schema.json +91 -0
- package/templates/content/en/Organization/site-owner.mdx +16 -0
- package/templates/content/en/Partner/prisma.mdx +31 -0
- package/templates/content/en/Partner/supabase.mdx +30 -0
- package/templates/content/en/Partner/vercel.mdx +28 -0
- package/templates/content/en/PartnersList/partners.mdx +22 -0
- package/templates/content/en/Person/founder.mdx +18 -0
- package/templates/content/en/Pricing/pricing.mdx +538 -0
- package/templates/content/en/Pricing/pricing.schema.json +209 -0
- package/templates/content/en/PrivacyPage/privacy.mdx +165 -0
- package/templates/content/en/PrivacyPage/privacy.schema.json +64 -0
- package/templates/content/en/Product/ailk.mdx +19 -0
- package/templates/content/en/Product/ailk.schema.json +94 -0
- package/templates/content/en/Product/api-launch-kit.mdx +26 -0
- package/templates/content/en/Product/launch-kit.mdx +28 -0
- package/templates/content/en/ProductsList/products.mdx +26 -0
- package/templates/content/en/Project/ailk-phase-2.mdx +54 -0
- package/templates/content/en/ProjectsList/projects.mdx +25 -0
- package/templates/content/en/ResourceDetail/aeo-scoring-guide.mdx +27 -0
- package/templates/content/en/ResourceDetail/content-model-whitepaper.mdx +28 -0
- package/templates/content/en/ResourceDetail/schema-org-quick-reference.mdx +39 -0
- package/templates/content/en/ResourcesHub/resources.mdx +22 -0
- package/templates/content/en/Service/consulting.mdx +22 -0
- package/templates/content/en/Service/implementation.mdx +30 -0
- package/templates/content/en/Service/launch-service.mdx +232 -0
- package/templates/content/en/Service/launch.mdx +228 -0
- package/templates/content/en/ServicesList/services.mdx +84 -0
- package/templates/content/en/SoftwareProduct/ailk-aeo.mdx +44 -0
- package/templates/content/en/SoftwareProduct/ailk-content-adapters.mdx +47 -0
- package/templates/content/en/SoftwareProduct/ailk-schema.mdx +49 -0
- package/templates/content/en/TeamList/team.mdx +63 -0
- package/templates/content/en/TeamMember/founder.mdx +72 -0
- package/templates/content/en/TermsPage/terms.mdx +19 -0
- package/templates/content/en/TermsPage/terms.schema.json +99 -0
- package/templates/content/en/TestimonialsList/testimonials.mdx +44 -0
- package/templates/content/en/UseCasesList/use-cases.mdx +30 -0
- package/templates/content/en/Whitepaper/ax-first-product-development.mdx +27 -0
- package/templates/content/en/Whitepaper/open-core-commercial-strategy.mdx +27 -0
- package/templates/content/en/Whitepaper/structured-data-for-answer-engines.mdx +31 -0
- package/templates/content/en-XA/AboutPage/about.mdx +8 -0
- package/templates/content/en-XA/FAQPage/faq.mdx +8 -0
- package/templates/content/en-XA/HomePage/home.mdx +8 -0
- package/templates/content/es/_site.mdx +30 -0
- package/templates/content/fr/_site.mdx +30 -0
- package/templates/content/ja/_site.mdx +30 -0
- package/templates/content/pt-BR/AboutPage/about.mdx +8 -0
- package/templates/content/pt-BR/FAQPage/faq.mdx +8 -0
- package/templates/content/pt-BR/HomePage/home.mdx +23 -0
- package/templates/database/CHANGELOG.md +5 -0
- package/templates/database/README.md +234 -0
- package/templates/database/__tests__/user-cascade.test.ts +125 -0
- package/templates/database/auth/.gitkeep +0 -0
- package/templates/database/config/.gitkeep +0 -0
- package/templates/database/content/.gitkeep +0 -0
- package/templates/database/inbox/.gitkeep +0 -0
- package/templates/database/inbox/README.md +3 -0
- package/templates/database/inbox/__tests__/schema.test.ts +40 -0
- package/templates/database/inbox/schema.prisma +52 -0
- package/templates/database/jest.config.cjs +29 -0
- package/templates/database/migrations/20260508000000_add_inbox_namespace/migration.sql +328 -0
- package/templates/database/migrations/20260513000000_better_auth_migration/migration.sql +100 -0
- package/templates/database/migrations/20260515000000_add_user_billing_fields/migration.sql +28 -0
- package/templates/database/migrations/20260516000000_add_user_tier_care_plan_seat/migration.sql +25 -0
- package/templates/database/migrations/20260518000000_add_telemetry_records/migration.sql +42 -0
- package/templates/database/migrations/20260525000000_theme_wiring/migration.sql +67 -0
- package/templates/database/migrations/20260710000000_retire_commercial_tier/migration.sql +21 -0
- package/templates/database/migrations/migration_lock.toml +3 -0
- package/templates/database/ops/.gitkeep +0 -0
- package/templates/database/package.json +45 -0
- package/templates/database/prisma.config.ts +20 -0
- package/templates/database/schema.prisma +487 -0
- package/templates/database/scripts/db-generate-locked.sh +80 -0
- package/templates/database/seed-test.ts +85 -0
- package/templates/database/tsconfig.build.json +16 -0
- package/templates/database/tsconfig.json +9 -0
- package/templates/package.json +125 -0
- package/templates/pnpm-workspace.yaml +6 -0
- package/templates/project.yaml +59 -0
- package/templates/tsconfig.base.json +18 -0
- package/templates/tsconfig.json +45 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file file-fallback.test.ts
|
|
3
|
+
* @description Unit tests for file-fallback.ts — writeLeadFallback
|
|
4
|
+
*
|
|
5
|
+
* Test path deviation from spec §3: spec states apps/api/__tests__/lib/,
|
|
6
|
+
* but jest.config.cjs roots at <rootDir>/src, so tests live at
|
|
7
|
+
* apps/api/src/lib/__tests__/ to match the existing repo convention.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from "fs/promises";
|
|
11
|
+
import * as os from "os";
|
|
12
|
+
import * as path from "path";
|
|
13
|
+
|
|
14
|
+
import { writeLeadFallback } from "../file-fallback.js";
|
|
15
|
+
import type { FallbackLead } from "../file-fallback.js";
|
|
16
|
+
|
|
17
|
+
function makeLead(overrides: Partial<FallbackLead> = {}): FallbackLead {
|
|
18
|
+
return {
|
|
19
|
+
id: "abc123de",
|
|
20
|
+
siteId: "site-001",
|
|
21
|
+
source: "oss-form",
|
|
22
|
+
email: "test@example.com",
|
|
23
|
+
payload: { message: "hello" },
|
|
24
|
+
createdAt: "2026-05-08T12:00:00.000Z",
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let tmpDir: string;
|
|
30
|
+
|
|
31
|
+
beforeEach(async () => {
|
|
32
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "file-fallback-test-"));
|
|
33
|
+
process.env.AILK_INBOX_FALLBACK_DIR = tmpDir;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(async () => {
|
|
37
|
+
delete process.env.AILK_INBOX_FALLBACK_DIR;
|
|
38
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("writeLeadFallback", () => {
|
|
42
|
+
it("happy path: writes <timestamp>-<id>.json; file content parses back to lead", async () => {
|
|
43
|
+
const lead = makeLead();
|
|
44
|
+
const result = await writeLeadFallback(lead);
|
|
45
|
+
|
|
46
|
+
expect(result).toMatchObject({ ok: true });
|
|
47
|
+
if (!result.ok) throw new Error("expected ok");
|
|
48
|
+
|
|
49
|
+
const raw = await fs.readFile(result.path, "utf8");
|
|
50
|
+
const parsed = JSON.parse(raw);
|
|
51
|
+
expect(parsed).toMatchObject({ id: lead.id, email: lead.email });
|
|
52
|
+
expect(result.path).toContain(`-${lead.id}.json`);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("directory creation: creates target dir if it does not exist", async () => {
|
|
56
|
+
const nested = path.join(tmpDir, "new-subdir", "inbox");
|
|
57
|
+
process.env.AILK_INBOX_FALLBACK_DIR = nested;
|
|
58
|
+
|
|
59
|
+
const lead = makeLead();
|
|
60
|
+
const result = await writeLeadFallback(lead);
|
|
61
|
+
|
|
62
|
+
expect(result).toMatchObject({ ok: true });
|
|
63
|
+
const stat = await fs.stat(nested);
|
|
64
|
+
expect(stat.isDirectory()).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("invalid createdAt falls back to current ISO timestamp", async () => {
|
|
68
|
+
const lead = makeLead({ createdAt: "not-a-date" });
|
|
69
|
+
const result = await writeLeadFallback(lead);
|
|
70
|
+
|
|
71
|
+
expect(result).toMatchObject({ ok: true });
|
|
72
|
+
if (!result.ok) throw new Error("expected ok");
|
|
73
|
+
|
|
74
|
+
// Filename must match the ISO-derived pattern: YYYY-MM-DDTHH-MM-SS.mmmZ-<id>.json
|
|
75
|
+
const filename = path.basename(result.path);
|
|
76
|
+
expect(filename).toMatch(
|
|
77
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{3}Z-abc123de\.json$/,
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("path traversal via lead.id: rejected with invalid_id (no fs touch)", async () => {
|
|
82
|
+
const lead = makeLead({ id: "../../etc/passwd" });
|
|
83
|
+
const result = await writeLeadFallback(lead);
|
|
84
|
+
|
|
85
|
+
expect(result).toMatchObject({ ok: false, error: { kind: "invalid_id" } });
|
|
86
|
+
const files = await fs.readdir(tmpDir);
|
|
87
|
+
expect(files).toHaveLength(0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("path traversal via env var: resolved path stays inside targetDir", async () => {
|
|
91
|
+
// Env var with traversal — operator-controlled but function validates
|
|
92
|
+
const resolved = path.resolve(
|
|
93
|
+
process.cwd(),
|
|
94
|
+
`${tmpDir}/legit/../../${path.basename(tmpDir)}`,
|
|
95
|
+
);
|
|
96
|
+
process.env.AILK_INBOX_FALLBACK_DIR = `${tmpDir}/legit/../../${path.basename(tmpDir)}`;
|
|
97
|
+
|
|
98
|
+
const lead = makeLead();
|
|
99
|
+
const result = await writeLeadFallback(lead);
|
|
100
|
+
|
|
101
|
+
// The resolved dir is valid — write succeeds; path stays inside resolved targetDir
|
|
102
|
+
expect(result).toMatchObject({ ok: true });
|
|
103
|
+
if (!result.ok) throw new Error("expected ok");
|
|
104
|
+
expect(result.path.startsWith(resolved)).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("filesystem error: returns fs_error result; does not throw", async () => {
|
|
108
|
+
// Make targetDir a file (not a directory) to force mkdir failure
|
|
109
|
+
await fs.rm(tmpDir, { recursive: true });
|
|
110
|
+
await fs.writeFile(tmpDir, "not-a-dir");
|
|
111
|
+
|
|
112
|
+
const lead = makeLead();
|
|
113
|
+
const result = await writeLeadFallback(lead);
|
|
114
|
+
|
|
115
|
+
expect(result).toMatchObject({ ok: false, error: { kind: "fs_error" } });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("Windows-compat: colons are replaced with hyphens in the timestamp portion of the filename", async () => {
|
|
119
|
+
const lead = makeLead({ createdAt: "2026-05-08T12:00:00.000Z" });
|
|
120
|
+
const result = await writeLeadFallback(lead);
|
|
121
|
+
|
|
122
|
+
expect(result).toMatchObject({ ok: true });
|
|
123
|
+
if (!result.ok) throw new Error("expected ok");
|
|
124
|
+
|
|
125
|
+
const filename = path.basename(result.path);
|
|
126
|
+
expect(filename).not.toContain(":");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("crafted createdAt traversal: ISO validator rejects non-ISO, falls back to current, writes inside targetDir", async () => {
|
|
130
|
+
const lead = makeLead({ createdAt: "../2026" });
|
|
131
|
+
const result = await writeLeadFallback(lead);
|
|
132
|
+
|
|
133
|
+
// ISO validator rejects '../2026' → falls back to new Date().toISOString()
|
|
134
|
+
// Write succeeds inside targetDir; no escape file created
|
|
135
|
+
expect(result).toMatchObject({ ok: true });
|
|
136
|
+
if (!result.ok) throw new Error("expected ok");
|
|
137
|
+
expect(result.path.startsWith(tmpDir)).toBe(true);
|
|
138
|
+
|
|
139
|
+
// Confirm no file was written outside tmpDir (escape path would be one level up)
|
|
140
|
+
const escapedDir = path.resolve(tmpDir, "..");
|
|
141
|
+
const escapedFile = path.join(escapedDir, `2026-${lead.id}.json`);
|
|
142
|
+
await expect(fs.stat(escapedFile)).rejects.toThrow();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file lead-capture-store.test.ts
|
|
3
|
+
* Unit tests for the lead-capture-store write path (Issue #27 Unit 1).
|
|
4
|
+
*
|
|
5
|
+
* All fs operations are mocked; no real disk I/O in these tests.
|
|
6
|
+
* Acceptance criteria covered (spec §5.1):
|
|
7
|
+
* AC1 — file write on every successful submission (always-on by default)
|
|
8
|
+
* AC3 — IPs default OFF; opt-in via LEAD_CAPTURE_INCLUDE_IP=true
|
|
9
|
+
* AC4 — LEAD_CAPTURE_FILE_DISABLED=true short-circuits write
|
|
10
|
+
* AC5 — LEAD_CAPTURE_FILE env drives output path
|
|
11
|
+
* AC7 — 50 concurrent writes land without corruption (POSIX O_APPEND atomicity)
|
|
12
|
+
* AC8 — startup-time directory check (tested in server.test.ts)
|
|
13
|
+
* AC9 — LeadCaptureRecord shape validated by Zod schema
|
|
14
|
+
* AC14 — appendLeadRecord resolves ok:false on fs error (never throws)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// ─── fs mock ─────────────────────────────────────────────────────────────────
|
|
18
|
+
const mockAppendFile = jest.fn();
|
|
19
|
+
const mockAccess = jest.fn();
|
|
20
|
+
const mockMkdir = jest.fn();
|
|
21
|
+
|
|
22
|
+
jest.mock("fs/promises", () => ({
|
|
23
|
+
appendFile: (...args: unknown[]) => mockAppendFile(...args),
|
|
24
|
+
access: (...args: unknown[]) => mockAccess(...args),
|
|
25
|
+
mkdir: (...args: unknown[]) => mockMkdir(...args),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
appendLeadRecord,
|
|
30
|
+
validateLeadCaptureDir,
|
|
31
|
+
} from "../lead-capture-store.js";
|
|
32
|
+
import type { LeadCaptureRecord } from "../lead-capture-store.js";
|
|
33
|
+
|
|
34
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
const record: LeadCaptureRecord = {
|
|
37
|
+
id: "cuid-test-1",
|
|
38
|
+
createdAt: "2024-01-01T00:00:00.000Z",
|
|
39
|
+
source: "contact",
|
|
40
|
+
data: { email: "test@example.com", message: "hello" },
|
|
41
|
+
metadata: {},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
describe("appendLeadRecord", () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
jest.clearAllMocks();
|
|
49
|
+
mockAppendFile.mockResolvedValue(undefined);
|
|
50
|
+
mockMkdir.mockResolvedValue(undefined);
|
|
51
|
+
// Clear lead-capture env vars
|
|
52
|
+
delete process.env.LEAD_CAPTURE_FILE;
|
|
53
|
+
delete process.env.LEAD_CAPTURE_FILE_DISABLED;
|
|
54
|
+
delete process.env.LEAD_CAPTURE_INCLUDE_IP;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
delete process.env.LEAD_CAPTURE_FILE;
|
|
59
|
+
delete process.env.LEAD_CAPTURE_FILE_DISABLED;
|
|
60
|
+
delete process.env.LEAD_CAPTURE_INCLUDE_IP;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// AC1 — always-on by default (no env set)
|
|
64
|
+
it("writes a JSONL line by default (always-on)", async () => {
|
|
65
|
+
const result = await appendLeadRecord(record);
|
|
66
|
+
|
|
67
|
+
expect(result.ok).toBe(true);
|
|
68
|
+
expect(mockAppendFile).toHaveBeenCalledTimes(1);
|
|
69
|
+
// Verify the written content is valid JSON with a newline
|
|
70
|
+
const [, content, opts] = mockAppendFile.mock.calls[0];
|
|
71
|
+
expect(opts).toMatchObject({ flag: "a" });
|
|
72
|
+
const parsed = JSON.parse(content.trimEnd());
|
|
73
|
+
expect(parsed.id).toBe(record.id);
|
|
74
|
+
expect(parsed.source).toBe(record.source);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// AC5 — LEAD_CAPTURE_FILE drives output path
|
|
78
|
+
it("uses LEAD_CAPTURE_FILE env for output path", async () => {
|
|
79
|
+
process.env.LEAD_CAPTURE_FILE = "/tmp/test-leads.jsonl";
|
|
80
|
+
const result = await appendLeadRecord(record);
|
|
81
|
+
|
|
82
|
+
expect(result.ok).toBe(true);
|
|
83
|
+
expect(mockAppendFile).toHaveBeenCalledWith(
|
|
84
|
+
"/tmp/test-leads.jsonl",
|
|
85
|
+
expect.any(String),
|
|
86
|
+
expect.objectContaining({ flag: "a" }),
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// AC5 — defaults to apps/api/leads.jsonl relative to cwd when unset
|
|
91
|
+
it("defaults to apps/api/leads.jsonl when LEAD_CAPTURE_FILE not set", async () => {
|
|
92
|
+
const result = await appendLeadRecord(record);
|
|
93
|
+
|
|
94
|
+
expect(result.ok).toBe(true);
|
|
95
|
+
const [filePath] = mockAppendFile.mock.calls[0];
|
|
96
|
+
expect(filePath).toMatch(/leads\.jsonl$/);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// AC4 — LEAD_CAPTURE_FILE_DISABLED=true skips the write
|
|
100
|
+
it("short-circuits write when LEAD_CAPTURE_FILE_DISABLED=true", async () => {
|
|
101
|
+
process.env.LEAD_CAPTURE_FILE_DISABLED = "true";
|
|
102
|
+
const result = await appendLeadRecord(record);
|
|
103
|
+
|
|
104
|
+
expect(result.ok).toBe(true);
|
|
105
|
+
expect((result as { ok: true; skipped: true }).skipped).toBe(true);
|
|
106
|
+
expect(mockAppendFile).not.toHaveBeenCalled();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// AC3 — IPs default OFF
|
|
110
|
+
it("does not include IP by default", async () => {
|
|
111
|
+
const recordWithIp: LeadCaptureRecord = {
|
|
112
|
+
...record,
|
|
113
|
+
metadata: { ip: "1.2.3.4" },
|
|
114
|
+
};
|
|
115
|
+
const result = await appendLeadRecord(recordWithIp);
|
|
116
|
+
|
|
117
|
+
expect(result.ok).toBe(true);
|
|
118
|
+
const [, content] = mockAppendFile.mock.calls[0];
|
|
119
|
+
const parsed = JSON.parse(content.trimEnd());
|
|
120
|
+
expect(parsed.metadata?.ip).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// AC3 — LEAD_CAPTURE_INCLUDE_IP=true includes the IP
|
|
124
|
+
it("includes IP when LEAD_CAPTURE_INCLUDE_IP=true", async () => {
|
|
125
|
+
process.env.LEAD_CAPTURE_INCLUDE_IP = "true";
|
|
126
|
+
const recordWithIp: LeadCaptureRecord = {
|
|
127
|
+
...record,
|
|
128
|
+
metadata: { ip: "1.2.3.4" },
|
|
129
|
+
};
|
|
130
|
+
const result = await appendLeadRecord(recordWithIp);
|
|
131
|
+
|
|
132
|
+
expect(result.ok).toBe(true);
|
|
133
|
+
const [, content] = mockAppendFile.mock.calls[0];
|
|
134
|
+
const parsed = JSON.parse(content.trimEnd());
|
|
135
|
+
expect(parsed.metadata?.ip).toBe("1.2.3.4");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// AC14 — never throws; returns ok:false on fs error
|
|
139
|
+
it("returns ok:false on fs error, never throws", async () => {
|
|
140
|
+
mockAppendFile.mockRejectedValue(new Error("ENOSPC: no space"));
|
|
141
|
+
|
|
142
|
+
const result = await appendLeadRecord(record);
|
|
143
|
+
|
|
144
|
+
expect(result.ok).toBe(false);
|
|
145
|
+
expect((result as { ok: false; error: string }).error).toMatch(/ENOSPC/);
|
|
146
|
+
await expect(appendLeadRecord(record)).resolves.toBeDefined();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Written content must be JSONL (one JSON object per line + newline)
|
|
150
|
+
it("writes valid JSONL format (one JSON object + trailing newline)", async () => {
|
|
151
|
+
await appendLeadRecord(record);
|
|
152
|
+
const [, content] = mockAppendFile.mock.calls[0];
|
|
153
|
+
expect(content).toMatch(/\n$/);
|
|
154
|
+
// Must parse as JSON
|
|
155
|
+
expect(() => JSON.parse(content.trimEnd())).not.toThrow();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// AC7 — 50-concurrent write integrity (atomicity via O_APPEND flag)
|
|
159
|
+
// We verify the flag is set and all 50 calls complete successfully.
|
|
160
|
+
// Real O_APPEND atomicity is kernel-guaranteed for local filesystems.
|
|
161
|
+
it("50 concurrent writes all resolve ok:true with flag:a", async () => {
|
|
162
|
+
const promises = Array.from({ length: 50 }, (_, i) =>
|
|
163
|
+
appendLeadRecord({ ...record, id: `cuid-${i}` }),
|
|
164
|
+
);
|
|
165
|
+
const results = await Promise.all(promises);
|
|
166
|
+
expect(results).toHaveLength(50);
|
|
167
|
+
results.forEach((r) => expect(r.ok).toBe(true));
|
|
168
|
+
// All calls use flag:'a'
|
|
169
|
+
mockAppendFile.mock.calls.forEach(([, , opts]) => {
|
|
170
|
+
expect(opts).toMatchObject({ flag: "a" });
|
|
171
|
+
});
|
|
172
|
+
expect(mockAppendFile).toHaveBeenCalledTimes(50);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Record shape must match LeadCaptureRecord (id, createdAt, source, data, metadata)
|
|
176
|
+
it("written JSONL contains all required top-level fields", async () => {
|
|
177
|
+
await appendLeadRecord(record);
|
|
178
|
+
const [, content] = mockAppendFile.mock.calls[0];
|
|
179
|
+
const parsed = JSON.parse(content.trimEnd());
|
|
180
|
+
expect(parsed).toHaveProperty("id");
|
|
181
|
+
expect(parsed).toHaveProperty("createdAt");
|
|
182
|
+
expect(parsed).toHaveProperty("source");
|
|
183
|
+
expect(parsed).toHaveProperty("data");
|
|
184
|
+
expect(parsed).toHaveProperty("metadata");
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("validateLeadCaptureDir", () => {
|
|
189
|
+
beforeEach(() => {
|
|
190
|
+
jest.clearAllMocks();
|
|
191
|
+
delete process.env.LEAD_CAPTURE_FILE;
|
|
192
|
+
delete process.env.LEAD_CAPTURE_FILE_DISABLED;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
afterEach(() => {
|
|
196
|
+
delete process.env.LEAD_CAPTURE_FILE;
|
|
197
|
+
delete process.env.LEAD_CAPTURE_FILE_DISABLED;
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// AC8 — no LEAD_CAPTURE_FILE set → no-op (validation passes)
|
|
201
|
+
it("resolves without error when LEAD_CAPTURE_FILE not set", async () => {
|
|
202
|
+
await expect(validateLeadCaptureDir()).resolves.toBeUndefined();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// AC8 — LEAD_CAPTURE_FILE_DISABLED → no-op
|
|
206
|
+
it("resolves without error when LEAD_CAPTURE_FILE_DISABLED=true", async () => {
|
|
207
|
+
process.env.LEAD_CAPTURE_FILE_DISABLED = "true";
|
|
208
|
+
process.env.LEAD_CAPTURE_FILE = "/nonexistent/dir/leads.jsonl";
|
|
209
|
+
await expect(validateLeadCaptureDir()).resolves.toBeUndefined();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// AC8 — directory exists → resolves
|
|
213
|
+
it("resolves when LEAD_CAPTURE_FILE directory exists", async () => {
|
|
214
|
+
process.env.LEAD_CAPTURE_FILE = "/tmp/exists/leads.jsonl";
|
|
215
|
+
mockAccess.mockResolvedValue(undefined);
|
|
216
|
+
await expect(validateLeadCaptureDir()).resolves.toBeUndefined();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// AC8 — directory missing → throws
|
|
220
|
+
it("throws when LEAD_CAPTURE_FILE directory does not exist", async () => {
|
|
221
|
+
process.env.LEAD_CAPTURE_FILE = "/nonexistent/dir/leads.jsonl";
|
|
222
|
+
mockAccess.mockRejectedValue(
|
|
223
|
+
Object.assign(new Error("ENOENT"), { code: "ENOENT" }),
|
|
224
|
+
);
|
|
225
|
+
await expect(validateLeadCaptureDir()).rejects.toThrow(/LEAD_CAPTURE_FILE/);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* outbound-http — fetch wrapper with timeout + SSRF allow-list.
|
|
3
|
+
*
|
|
4
|
+
* Every outbound HTTP call originating from apps/api/src/lib goes through this
|
|
5
|
+
* wrapper. Two guarantees:
|
|
6
|
+
*
|
|
7
|
+
* 1. Default 10s timeout via AbortSignal.timeout
|
|
8
|
+
* 2. SSRF allow-list — RFC 1918 / loopback / link-local addresses are rejected
|
|
9
|
+
* unless explicitly allowed (test-mode escape hatch only)
|
|
10
|
+
*
|
|
11
|
+
* The wrapper is forward-looking — apps/api/src/lib currently has no outbound
|
|
12
|
+
* fetch callsites — so the wrapper exists and tests pin its contract before
|
|
13
|
+
* the first consumer arrives.
|
|
14
|
+
*/
|
|
15
|
+
import { ssrfBlocked, outboundFetch } from "../outbound-http.js";
|
|
16
|
+
|
|
17
|
+
describe("outbound-http — SSRF allow-list", () => {
|
|
18
|
+
it.each([
|
|
19
|
+
["http://127.0.0.1/path", "loopback"],
|
|
20
|
+
["http://127.255.0.1/path", "loopback"],
|
|
21
|
+
["http://localhost/path", "loopback hostname"],
|
|
22
|
+
["http://10.0.0.1/", "RFC 1918 10/8"],
|
|
23
|
+
["http://10.255.255.255/", "RFC 1918 10/8 edge"],
|
|
24
|
+
["http://172.16.0.1/", "RFC 1918 172.16/12"],
|
|
25
|
+
["http://172.31.255.255/", "RFC 1918 172.16/12 edge"],
|
|
26
|
+
["http://192.168.0.1/", "RFC 1918 192.168/16"],
|
|
27
|
+
["http://192.168.255.255/", "RFC 1918 192.168/16 edge"],
|
|
28
|
+
["http://169.254.169.254/latest/meta-data/", "AWS metadata link-local"],
|
|
29
|
+
["http://169.254.0.1/", "link-local"],
|
|
30
|
+
["http://[::1]/", "IPv6 loopback"],
|
|
31
|
+
["http://0.0.0.0/", "all-interfaces wildcard"],
|
|
32
|
+
])("rejects %s (%s)", (url) => {
|
|
33
|
+
expect(ssrfBlocked(url)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it.each([
|
|
37
|
+
["https://api.stripe.com/v1/x", "public Stripe"],
|
|
38
|
+
["https://api.example.com/path", "generic public host"],
|
|
39
|
+
["http://8.8.8.8/", "public IP"],
|
|
40
|
+
["https://172.15.0.1/", "172.15 is OUT of RFC 1918 172.16/12"],
|
|
41
|
+
["https://172.32.0.1/", "172.32 is OUT of RFC 1918 172.16/12"],
|
|
42
|
+
])("allows %s (%s)", (url) => {
|
|
43
|
+
expect(ssrfBlocked(url)).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("rejects non-HTTP schemes", () => {
|
|
47
|
+
expect(ssrfBlocked("file:///etc/passwd")).toBe(true);
|
|
48
|
+
expect(ssrfBlocked("ftp://example.com/")).toBe(true);
|
|
49
|
+
expect(ssrfBlocked("javascript:alert(1)")).toBe(true);
|
|
50
|
+
expect(ssrfBlocked("gopher://example.com/")).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("rejects unparseable URLs", () => {
|
|
54
|
+
expect(ssrfBlocked("not a url")).toBe(true);
|
|
55
|
+
expect(ssrfBlocked("")).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("outbound-http — outboundFetch", () => {
|
|
60
|
+
let originalFetch: typeof globalThis.fetch;
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
originalFetch = globalThis.fetch;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
globalThis.fetch = originalFetch;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("rejects SSRF-blocked URLs before issuing a network call", async () => {
|
|
71
|
+
const fetchSpy: jest.Mock<ReturnType<typeof globalThis.fetch>, Parameters<typeof globalThis.fetch>> =
|
|
72
|
+
jest.fn();
|
|
73
|
+
globalThis.fetch = fetchSpy;
|
|
74
|
+
|
|
75
|
+
await expect(outboundFetch("http://127.0.0.1/admin")).rejects.toThrow(
|
|
76
|
+
/SSRF|disallowed|forbidden/i,
|
|
77
|
+
);
|
|
78
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("issues fetch on an allowed URL with a default timeout signal", async () => {
|
|
82
|
+
const fetchSpy: typeof globalThis.fetch = jest.fn(async (_url, init) => {
|
|
83
|
+
// confirm AbortSignal is wired
|
|
84
|
+
expect((init as RequestInit | undefined)?.signal).toBeDefined();
|
|
85
|
+
return new Response("ok", { status: 200 });
|
|
86
|
+
});
|
|
87
|
+
globalThis.fetch = fetchSpy;
|
|
88
|
+
|
|
89
|
+
const res = await outboundFetch("https://api.example.com/x");
|
|
90
|
+
expect(res.status).toBe(200);
|
|
91
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("aborts when timeout elapses before fetch resolves", async () => {
|
|
95
|
+
// Mock fetch to honor the abort signal — simulates a real slow server.
|
|
96
|
+
const slowFetch: typeof globalThis.fetch = async (_url, init) => {
|
|
97
|
+
return new Promise((_resolve, reject) => {
|
|
98
|
+
(init as RequestInit | undefined)?.signal?.addEventListener(
|
|
99
|
+
"abort",
|
|
100
|
+
() => {
|
|
101
|
+
reject(new DOMException("aborted", "AbortError"));
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
globalThis.fetch = slowFetch;
|
|
107
|
+
|
|
108
|
+
await expect(
|
|
109
|
+
outboundFetch("https://api.example.com/slow", { timeoutMs: 50 }),
|
|
110
|
+
).rejects.toThrow(/abort|timeout/i);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("respects the per-call allowPrivate escape hatch (test/dev only)", async () => {
|
|
114
|
+
const fetchSpy: typeof globalThis.fetch = jest.fn(
|
|
115
|
+
async () => new Response("ok", { status: 200 }),
|
|
116
|
+
);
|
|
117
|
+
globalThis.fetch = fetchSpy;
|
|
118
|
+
|
|
119
|
+
const res = await outboundFetch("http://127.0.0.1/local-svc", {
|
|
120
|
+
allowPrivate: true,
|
|
121
|
+
});
|
|
122
|
+
expect(res.status).toBe(200);
|
|
123
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the prisma.ts singleton — verify construction, log routing,
|
|
3
|
+
* and globalThis caching behaviour without touching the real adapter or DB.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const mockPrismaInstance = {
|
|
7
|
+
$connect: jest.fn(),
|
|
8
|
+
$disconnect: jest.fn(),
|
|
9
|
+
user: {},
|
|
10
|
+
apiToken: {},
|
|
11
|
+
userData: {},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const MockPrismaClient = jest.fn().mockImplementation((options) => {
|
|
15
|
+
expect(options).toBeDefined();
|
|
16
|
+
expect(options.adapter).toBeDefined();
|
|
17
|
+
return mockPrismaInstance;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Mock @prisma/adapter-pg so the import in prisma.ts doesn't construct a
|
|
21
|
+
// real pg.Pool against DATABASE_URL.
|
|
22
|
+
const MockPrismaPg = jest.fn().mockImplementation((config) => {
|
|
23
|
+
expect(config).toBeDefined();
|
|
24
|
+
expect(config.connectionString).toBeDefined();
|
|
25
|
+
return { __mockAdapter: true };
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
jest.mock('@prisma/adapter-pg', () => ({
|
|
29
|
+
PrismaPg: MockPrismaPg,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
jest.mock('@working-theory/database', () => ({
|
|
33
|
+
PrismaClient: MockPrismaClient,
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
describe('Prisma Client', () => {
|
|
37
|
+
const originalEnv = process.env;
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
jest.resetModules();
|
|
41
|
+
process.env = { ...originalEnv };
|
|
42
|
+
// Clear global prisma
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- #506/#507: pre-existing test-hygiene violation
|
|
44
|
+
(globalThis as any).prisma = undefined;
|
|
45
|
+
jest.clearAllMocks();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
process.env = originalEnv;
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- #506/#507: pre-existing test-hygiene violation
|
|
51
|
+
(globalThis as any).prisma = undefined;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('PrismaClient initialization', () => {
|
|
55
|
+
it('should create new PrismaClient instance when globalThis.prisma is undefined', () => {
|
|
56
|
+
// ARRANGE
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- #506/#507: pre-existing test-hygiene violation
|
|
58
|
+
(globalThis as any).prisma = undefined;
|
|
59
|
+
process.env.NODE_ENV = 'test';
|
|
60
|
+
|
|
61
|
+
// ACT
|
|
62
|
+
const { prisma } = require('../prisma');
|
|
63
|
+
|
|
64
|
+
// ASSERT
|
|
65
|
+
expect(prisma).toBeDefined();
|
|
66
|
+
const { PrismaClient } = require('@working-theory/database');
|
|
67
|
+
expect(PrismaClient).toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should use existing globalThis.prisma instance if available', () => {
|
|
71
|
+
// ARRANGE
|
|
72
|
+
const mockPrisma = { $connect: jest.fn(), user: {} };
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- #506/#507: pre-existing test-hygiene violation
|
|
74
|
+
(globalThis as any).prisma = mockPrisma;
|
|
75
|
+
|
|
76
|
+
// ACT
|
|
77
|
+
const { prisma } = require('../prisma');
|
|
78
|
+
|
|
79
|
+
// ASSERT
|
|
80
|
+
expect(prisma).toBe(mockPrisma);
|
|
81
|
+
const { PrismaClient } = require('@working-theory/database');
|
|
82
|
+
expect(PrismaClient).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should configure logging for development environment', () => {
|
|
86
|
+
// ARRANGE
|
|
87
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- #506/#507: pre-existing test-hygiene violation
|
|
88
|
+
(globalThis as any).prisma = undefined;
|
|
89
|
+
process.env.NODE_ENV = 'development';
|
|
90
|
+
|
|
91
|
+
// ACT
|
|
92
|
+
require('../prisma');
|
|
93
|
+
|
|
94
|
+
// ASSERT
|
|
95
|
+
const { PrismaClient } = require('@working-theory/database');
|
|
96
|
+
expect(PrismaClient).toHaveBeenCalledWith({
|
|
97
|
+
adapter: expect.objectContaining({ __mockAdapter: true }),
|
|
98
|
+
log: ['query', 'error', 'warn'],
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should configure minimal logging for production environment', () => {
|
|
103
|
+
// ARRANGE
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- #506/#507: pre-existing test-hygiene violation
|
|
105
|
+
(globalThis as any).prisma = undefined;
|
|
106
|
+
process.env.NODE_ENV = 'production';
|
|
107
|
+
|
|
108
|
+
// ACT
|
|
109
|
+
require('../prisma');
|
|
110
|
+
|
|
111
|
+
// ASSERT
|
|
112
|
+
const { PrismaClient } = require('@working-theory/database');
|
|
113
|
+
expect(PrismaClient).toHaveBeenCalledWith({
|
|
114
|
+
adapter: expect.objectContaining({ __mockAdapter: true }),
|
|
115
|
+
log: ['error'],
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should configure minimal logging for test environment', () => {
|
|
120
|
+
// ARRANGE
|
|
121
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- #506/#507: pre-existing test-hygiene violation
|
|
122
|
+
(globalThis as any).prisma = undefined;
|
|
123
|
+
process.env.NODE_ENV = 'test';
|
|
124
|
+
|
|
125
|
+
// ACT
|
|
126
|
+
require('../prisma');
|
|
127
|
+
|
|
128
|
+
// ASSERT
|
|
129
|
+
const { PrismaClient } = require('@working-theory/database');
|
|
130
|
+
expect(PrismaClient).toHaveBeenCalledWith({
|
|
131
|
+
adapter: expect.objectContaining({ __mockAdapter: true }),
|
|
132
|
+
log: ['error'],
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should store prisma instance in globalThis for non-production environments', () => {
|
|
137
|
+
// ARRANGE
|
|
138
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- #506/#507: pre-existing test-hygiene violation
|
|
139
|
+
(globalThis as any).prisma = undefined;
|
|
140
|
+
process.env.NODE_ENV = 'development';
|
|
141
|
+
|
|
142
|
+
// ACT
|
|
143
|
+
const { prisma } = require('../prisma');
|
|
144
|
+
|
|
145
|
+
// ASSERT
|
|
146
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- #506/#507: pre-existing test-hygiene violation
|
|
147
|
+
expect((globalThis as any).prisma).toBe(prisma);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should not store prisma instance in globalThis for production', () => {
|
|
151
|
+
// ARRANGE
|
|
152
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- #506/#507: pre-existing test-hygiene violation
|
|
153
|
+
(globalThis as any).prisma = undefined;
|
|
154
|
+
process.env.NODE_ENV = 'production';
|
|
155
|
+
|
|
156
|
+
// ACT
|
|
157
|
+
const { prisma } = require('../prisma');
|
|
158
|
+
|
|
159
|
+
// ASSERT
|
|
160
|
+
// In production, it should not be stored in globalThis
|
|
161
|
+
// But the instance should still be created
|
|
162
|
+
expect(prisma).toBeDefined();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|