create-ailk 0.18.0 → 0.19.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/LICENSE +6 -5
- package/NOTICE +2 -0
- package/dist/add-module.js +11 -3
- package/dist/category-profile.d.ts +10 -0
- package/dist/category-profile.js +21 -0
- package/dist/derive-architecture.d.ts +5 -1
- package/dist/derive-architecture.js +8 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/license-key-files.d.ts +6 -3
- package/dist/lib/license-key-files.js +8 -2
- package/dist/lib/scaffold-render-sweep.d.ts +32 -0
- package/dist/lib/scaffold-render-sweep.js +76 -3
- package/dist/license-set.d.ts +1 -1
- package/dist/license-set.js +13 -8
- package/dist/module-architecture.js +128 -3
- package/dist/programmatic.js +11 -1
- package/dist/site-roster.js +21 -4
- package/dist/spoke-checkout.d.ts +11 -3
- package/dist/spoke-checkout.js +62 -12
- package/dist/starter-grid.d.ts +1 -1
- package/dist/surfaces.d.ts +14 -0
- package/dist/surfaces.js +170 -1
- package/dist/template-manifest.d.ts +4 -3
- package/dist/topology.d.ts +7 -2
- package/dist/topology.js +88 -3
- package/package.json +5 -4
- package/templates/.env.example +3 -3
- package/templates/.github/workflows/ci.yml +5 -0
- package/templates/apps/api/.env.example +17 -1
- package/templates/apps/api/CLAUDE.md +31 -7
- package/templates/apps/api/LICENSE +6 -5
- package/templates/apps/api/NOTICE +2 -0
- package/templates/apps/api/jest.config.cjs +4 -29
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +19 -0
- package/templates/apps/api/src/__tests__/stripe-boot.test.ts +132 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +17 -0
- package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +237 -0
- package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +334 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +38 -0
- package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +217 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +7 -0
- package/templates/apps/api/src/lib/consent-store.ts +335 -0
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +5 -4
- package/templates/apps/api/src/lib/stripe.ts +176 -3
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +272 -39
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +51 -0
- package/templates/apps/api/src/middleware/auth.ts +214 -46
- package/templates/apps/api/src/middleware/rate-limit.ts +5 -0
- package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +167 -0
- package/templates/apps/api/src/routes/checkout/session.ts +3 -3
- package/templates/apps/api/src/routes/checkout/sessions.ts +6 -4
- package/templates/apps/api/src/routes/consent/cookie.ts +90 -0
- package/templates/apps/api/src/routes/consent/index.ts +18 -0
- package/templates/apps/api/src/routes/consent/unsubscribe.ts +127 -0
- package/templates/apps/api/src/routes/leads/index.ts +19 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +5 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +17 -4
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +2 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +159 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +158 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +2 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +17 -0
- package/templates/apps/api/src/server.ts +218 -16
- package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +503 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +18 -3
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +173 -4
- package/templates/apps/api/src/services/__tests__/person.test.ts +392 -0
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +87 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +57 -17
- package/templates/apps/api/src/services/consent.ts +488 -3
- package/templates/apps/api/src/services/lead-routing.ts +135 -14
- package/templates/apps/api/src/services/person.ts +501 -1
- package/templates/apps/api/src/services/project-listings.ts +7 -2
- package/templates/apps/api/src/services/request-confirm.ts +52 -0
- package/templates/apps/api/src/services/revalidation.ts +11 -1
- package/templates/apps/api/vercel.json +2 -2
- package/templates/apps/mcp/.env.example +10 -0
- package/templates/apps/mcp/CLAUDE.md +9 -4
- package/templates/apps/mcp/LICENSE +6 -5
- package/templates/apps/mcp/NOTICE +2 -0
- package/templates/apps/mcp/__tests__/abuse/guard.test.ts +198 -0
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +15 -3
- package/templates/apps/mcp/__tests__/boot.test.ts +78 -1
- package/templates/apps/mcp/__tests__/http-transport.test.ts +291 -2
- package/templates/apps/mcp/__tests__/request_callback.test.ts +69 -7
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +69 -8
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +69 -7
- package/templates/apps/mcp/jest.config.cjs +3 -44
- package/templates/apps/mcp/src/abuse/index.ts +117 -21
- package/templates/apps/mcp/src/boot.ts +5 -23
- package/templates/apps/mcp/src/http-transport.ts +93 -73
- package/templates/apps/mcp/src/index.ts +2 -0
- package/templates/apps/mcp/src/request-context.ts +52 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +9 -1
- package/templates/apps/mcp/src/tools/request_callback.ts +9 -1
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +9 -1
- package/templates/apps/web/LICENSE +6 -5
- package/templates/apps/web/NOTICE +2 -0
- package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/account-frame-sign-out.test.tsx +175 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +302 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +724 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +346 -41
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +11 -13
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +146 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +232 -29
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +305 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-popups.test.tsx +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +140 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +163 -72
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +451 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/security.test.tsx +409 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +65 -380
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/support.test.tsx +16 -11
- package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +1 -1
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +188 -54
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +24 -45
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +253 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +145 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +100 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +179 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +514 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +288 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +123 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +174 -0
- package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +172 -0
- package/templates/apps/web/app/[locale]/(authed)/account/billing-source.ts +147 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/request-quote.tsx +229 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/schedule-service.tsx +276 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/send-message.tsx +128 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/billing-anchor.tsx +27 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/contact-actions.ts +81 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/deletion-request.tsx +132 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +193 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/profile-form.tsx +104 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/profile-source.ts +73 -0
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +18 -8
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +4 -6
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +30 -4
- package/templates/apps/web/app/[locale]/(authed)/account/new-actions.ts +219 -0
- package/templates/apps/web/app/[locale]/(authed)/account/new-menu.tsx +340 -0
- package/templates/apps/web/app/[locale]/(authed)/account/new-popups.tsx +107 -0
- package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +57 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +85 -26
- package/templates/apps/web/app/[locale]/(authed)/account/profile-actions.ts +116 -0
- package/templates/apps/web/app/[locale]/(authed)/account/security/page.tsx +132 -0
- package/templates/apps/web/app/[locale]/(authed)/account/security/security-panel.tsx +102 -0
- package/templates/apps/web/app/[locale]/(authed)/account/security/security-source.ts +62 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +13 -354
- package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +116 -0
- package/templates/apps/web/app/[locale]/(authed)/account/sign-in-method.ts +37 -0
- package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +9 -10
- package/templates/apps/web/app/[locale]/layout.tsx +23 -4
- package/templates/apps/web/app/api/auth/hub/__tests__/continuation.test.ts +143 -0
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +62 -10
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +22 -7
- package/templates/apps/web/app/api/auth/hub/continuation.ts +73 -0
- package/templates/apps/web/app/api/auth/hub/continue/[ref]/__tests__/route.test.ts +188 -0
- package/templates/apps/web/app/api/auth/hub/continue/[ref]/route.ts +53 -0
- package/templates/apps/web/app/robots.txt/route.ts +10 -4
- package/templates/apps/web/auth.ts +25 -2
- package/templates/apps/web/lib/__tests__/account-config.test.ts +258 -46
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +84 -0
- package/templates/apps/web/lib/account-config.ts +188 -52
- package/templates/apps/web/lib/auth-email.ts +45 -10
- package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +143 -0
- package/templates/apps/web/lib/hub-session/index.ts +1 -0
- package/templates/apps/web/lib/hub-session/reseal.ts +90 -0
- package/templates/apps/web/lib/hub-session/seal.ts +13 -9
- package/templates/apps/web/lib/topology.generated.ts +33 -0
- package/templates/apps/web/messages/en.json +194 -33
- package/templates/database/CHANGELOG.md +37 -0
- package/templates/database/LICENSE +6 -5
- package/templates/database/NOTICE +2 -0
- package/templates/database/README.md +1 -1
- package/templates/database/__tests__/consent-ledger.db.test.ts +345 -0
- package/templates/database/__tests__/fence-fan-out.db.test.ts +6 -2
- package/templates/database/__tests__/fixtures/person-contact-backfill.json +182 -0
- package/templates/database/__tests__/member-area.db.test.ts +15 -3
- package/templates/database/__tests__/member-contract-migration.test.ts +85 -0
- package/templates/database/__tests__/member-contract.db.test.ts +413 -0
- package/templates/database/__tests__/person-contact.db.test.ts +563 -0
- package/templates/database/__tests__/person.db.test.ts +3 -1
- package/templates/database/__tests__/seed.test.ts +54 -2
- package/templates/database/inbox/schema.prisma +166 -25
- package/templates/database/migrations/20261012120000_member_contract/migration.sql +192 -0
- package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +20 -0
- package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +122 -0
- package/templates/database/migrations/20261014120000_person_contact/migration.sql +231 -0
- package/templates/database/package.json +3 -2
- package/templates/database/seed.ts +34 -2
- package/versions.json +36 -36
- package/templates/apps/api/src/__tests__/content-write-production-boot.test.ts +0 -104
- package/templates/apps/api/src/__tests__/cors.test.ts +0 -276
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +0 -279
- package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +0 -122
- package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +0 -167
- package/templates/apps/api/src/__tests__/root-modules.test.ts +0 -175
- package/templates/apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts +0 -117
- package/templates/apps/api/src/__tests__/server.test.ts +0 -591
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +0 -175
- package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +0 -293
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +0 -1620
- package/templates/apps/api/src/routes/project-listings/__tests__/directory.test.ts +0 -528
- package/templates/apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts +0 -198
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +0 -652
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -408
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +0 -476
- package/templates/apps/mcp/__tests__/audit.test.ts +0 -346
- package/templates/apps/mcp/__tests__/parity.test.ts +0 -325
- package/templates/apps/mcp/__tests__/workspace-surface.test.ts +0 -364
- package/templates/apps/web/app/[locale]/(authed)/account/settings/color-mode.tsx +0 -68
- package/templates/apps/web/app/[locale]/(authed)/account/settings/deletion-request.tsx +0 -55
- package/templates/apps/web/app/[locale]/(authed)/account/settings/name-form.tsx +0 -118
- package/templates/database/__tests__/build.test.ts +0 -131
|
@@ -1,407 +1,92 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for /[locale]/account/settings — User settings
|
|
3
|
-
*
|
|
2
|
+
* Tests for /[locale]/account/settings — now a redirect to User settings ›
|
|
3
|
+
* General (#8232, spec docs/specs/8232-member-settings-account-general-and-spec.md
|
|
4
|
+
* D8, D15; AC-6).
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* when the password method is on.
|
|
17
|
-
* 6. Privacy links to the cookie settings and files a deletion request.
|
|
6
|
+
* The one-page, anchored settings of #7430 split into `/account/general`
|
|
7
|
+
* and `/account/billing`; its General, Security, Notifications, Privacy and
|
|
8
|
+
* Billing cases moved to `general.test.tsx` and `billing.test.tsx`, rewritten
|
|
9
|
+
* for D2–D9. A `#billing` fragment never reaches the server, so the server
|
|
10
|
+
* redirect is to General and `BillingAnchor` forwards the fragment there
|
|
11
|
+
* (`general.test.tsx` § AC-6).
|
|
12
|
+
*
|
|
13
|
+
* #8441 — `SETTINGS_ROUTES` (settings-frame.tsx) is the one list of User
|
|
14
|
+
* settings' routes: the tabs read it, and so does `isSettingsPath`, the
|
|
15
|
+
* exact, locale-aware membership test the account frame takes the settings
|
|
16
|
+
* routes out of its shell with (layout.test.tsx renders each route).
|
|
18
17
|
*/
|
|
19
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
20
|
-
import { tmpdir } from "node:os";
|
|
21
|
-
import { join } from "node:path";
|
|
22
|
-
|
|
23
|
-
import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
|
24
|
-
|
|
25
|
-
jest.mock("next-intl/server", () => {
|
|
26
|
-
const msgs: unknown = require("../../../../../messages/en.json");
|
|
27
|
-
const dig = (obj: unknown, path: string): unknown =>
|
|
28
|
-
path.split(".").reduce<unknown>(
|
|
29
|
-
(acc, k) => (acc !== null && typeof acc === "object" ? (acc as Record<string, unknown>)[k] : undefined),
|
|
30
|
-
obj,
|
|
31
|
-
);
|
|
32
|
-
return {
|
|
33
|
-
__esModule: true,
|
|
34
|
-
getTranslations: async (arg: string | { namespace?: string; locale?: string }) => {
|
|
35
|
-
const ns = typeof arg === "string" ? arg : (arg.namespace ?? "");
|
|
36
|
-
const messages = dig(msgs, ns);
|
|
37
|
-
return (key: string, values?: Record<string, unknown>): string => {
|
|
38
|
-
const value = dig(messages, key);
|
|
39
|
-
if (typeof value !== "string") return `${ns}.${key}`;
|
|
40
|
-
return value.replace(/\{(\w+)\}/g, (m, name: string) =>
|
|
41
|
-
values && name in values ? String(values[name]) : m,
|
|
42
|
-
);
|
|
43
|
-
};
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
jest.mock("next/headers", () => ({
|
|
49
|
-
__esModule: true,
|
|
50
|
-
headers: jest.fn().mockResolvedValue(new Headers()),
|
|
51
|
-
cookies: jest.fn().mockResolvedValue({ toString: () => "session=abc" }),
|
|
52
|
-
}));
|
|
53
|
-
|
|
54
|
-
const mockGetSession = jest.fn();
|
|
55
|
-
// eslint-disable-next-line no-restricted-syntax -- #507: mock-internal rewrite pending; auth.ts's lazy Better-Auth singleton has no HTTP/DB boundary a unit test of the session fields can mock without standing up Prisma.
|
|
56
|
-
jest.mock("../../../../../auth", () => ({
|
|
57
|
-
__esModule: true,
|
|
58
|
-
auth: { api: { getSession: (...args: unknown[]) => mockGetSession(...args) } },
|
|
59
|
-
}));
|
|
60
|
-
|
|
61
|
-
const mockSubscriptionMe = jest.fn();
|
|
62
|
-
const mockLeadsSubmit = jest.fn();
|
|
63
|
-
jest.mock("@working-theory/api-client", () => ({
|
|
64
|
-
__esModule: true,
|
|
65
|
-
waitlistSignups: { me: jest.fn() },
|
|
66
|
-
flowCheckouts: { me: jest.fn() },
|
|
67
|
-
subscription: {
|
|
68
|
-
me: (...args: unknown[]) => mockSubscriptionMe(...args),
|
|
69
|
-
portalSession: jest.fn(),
|
|
70
|
-
},
|
|
71
|
-
leads: { submit: (...args: unknown[]) => mockLeadsSubmit(...args) },
|
|
72
|
-
visitorForwardHeaders: () => null,
|
|
73
|
-
}));
|
|
74
|
-
|
|
75
|
-
const mockUpdateUser = jest.fn();
|
|
76
|
-
jest.mock("better-auth/react", () => ({
|
|
77
|
-
__esModule: true,
|
|
78
|
-
createAuthClient: () => ({
|
|
79
|
-
signOut: jest.fn(),
|
|
80
|
-
updateUser: (...args: unknown[]) => mockUpdateUser(...args),
|
|
81
|
-
}),
|
|
82
|
-
}));
|
|
83
18
|
|
|
84
|
-
const
|
|
19
|
+
const mockRedirect = jest.fn();
|
|
85
20
|
jest.mock("next/navigation", () => ({
|
|
86
21
|
__esModule: true,
|
|
87
|
-
|
|
88
|
-
usePathname: () => "/account/settings",
|
|
89
|
-
redirect: jest.fn(),
|
|
22
|
+
redirect: (...args: unknown[]) => mockRedirect(...args),
|
|
90
23
|
}));
|
|
91
24
|
|
|
92
|
-
// The colour-mode cases test the theme's modes, so they run against an unpinned
|
|
93
|
-
// chrome: a scaffold whose design pins light mode (minimalism, #7742 D2) would
|
|
94
|
-
// otherwise hide the control in every case. theme-modes.test.ts covers the pin.
|
|
95
|
-
// eslint-disable-next-line no-restricted-syntax -- #507: `site-chrome.ts` is a generated data module with no external boundary to mock instead (as theme-modes.test.ts).
|
|
96
|
-
jest.mock("../../../../../lib/site-chrome", () => {
|
|
97
|
-
const actual = jest.requireActual("../../../../../lib/site-chrome");
|
|
98
|
-
const { colorMode, ...unpinned } = actual.siteChrome;
|
|
99
|
-
return { ...actual, siteChrome: unpinned };
|
|
100
|
-
});
|
|
101
|
-
|
|
102
25
|
import Page from "../settings/page";
|
|
103
|
-
|
|
104
|
-
const SESSION = {
|
|
105
|
-
user: {
|
|
106
|
-
id: "user_1",
|
|
107
|
-
name: "Ada Lovelace",
|
|
108
|
-
email: "ada@example.com",
|
|
109
|
-
emailVerified: true,
|
|
110
|
-
createdAt: new Date("2026-09-01T00:00:00.000Z"),
|
|
111
|
-
},
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const subscriptionBody = (body: Record<string, unknown>) => ({
|
|
115
|
-
ok: true,
|
|
116
|
-
body: { actionRequired: false, status: null, accessRevokesAt: null, portalAvailable: false, ...body },
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// ── The site's theme, as `_site.mdx` declares it ──────────────────────────────
|
|
120
|
-
|
|
121
|
-
let contentRoot: string;
|
|
122
|
-
|
|
123
|
-
function writeSite(frontmatter: string): void {
|
|
124
|
-
writeFileSync(join(contentRoot, "_site.mdx"), `---\n${frontmatter}\n---\n`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const ENV_KEYS = [
|
|
128
|
-
"AILK_CONTENT_ROOT",
|
|
129
|
-
"GOOGLE_CLIENT_ID",
|
|
130
|
-
"GOOGLE_CLIENT_SECRET",
|
|
131
|
-
"GITHUB_CLIENT_ID",
|
|
132
|
-
"GITHUB_CLIENT_SECRET",
|
|
133
|
-
"AUTH_EMAIL_SERVER",
|
|
134
|
-
"AUTH_EMAIL_FROM",
|
|
135
|
-
"AUTH_EMAIL_WEBHOOK_SECRET",
|
|
136
|
-
"BETTER_AUTH_URL",
|
|
137
|
-
"BETTER_AUTH_SECRET",
|
|
138
|
-
] as const;
|
|
139
|
-
const savedEnv: Partial<Record<(typeof ENV_KEYS)[number], string>> = {};
|
|
140
|
-
|
|
141
|
-
beforeAll(() => {
|
|
142
|
-
for (const key of ENV_KEYS) {
|
|
143
|
-
const value = process.env[key];
|
|
144
|
-
if (value !== undefined) savedEnv[key] = value;
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
afterAll(() => {
|
|
149
|
-
for (const key of ENV_KEYS) {
|
|
150
|
-
const value = savedEnv[key];
|
|
151
|
-
if (value === undefined) delete process.env[key];
|
|
152
|
-
else process.env[key] = value;
|
|
153
|
-
}
|
|
154
|
-
});
|
|
26
|
+
import { isSettingsPath, memberSettingsFrame, SETTINGS_ROUTES, type Translate } from "../settings-frame";
|
|
155
27
|
|
|
156
28
|
beforeEach(() => {
|
|
157
|
-
|
|
158
|
-
contentRoot = mkdtempSync(join(tmpdir(), "ailk-settings-"));
|
|
159
|
-
process.env.AILK_CONTENT_ROOT = contentRoot;
|
|
160
|
-
writeSite('siteName: "Test"');
|
|
161
|
-
mockGetSession.mockReset().mockResolvedValue(SESSION);
|
|
162
|
-
mockSubscriptionMe.mockReset().mockResolvedValue(subscriptionBody({}));
|
|
163
|
-
mockLeadsSubmit.mockReset().mockResolvedValue({ ok: true, body: { id: "lead_1", persisted: "db" } });
|
|
164
|
-
mockUpdateUser.mockReset().mockResolvedValue({ data: { status: true }, error: null });
|
|
165
|
-
mockRefresh.mockReset();
|
|
29
|
+
mockRedirect.mockReset();
|
|
166
30
|
});
|
|
167
31
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const element = await Page({ params: Promise.resolve({ locale: "en" }) });
|
|
174
|
-
render(element);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function sectionTitles(): string[] {
|
|
178
|
-
return screen
|
|
179
|
-
.getAllByRole("heading", { level: 2 })
|
|
180
|
-
.map((heading) => heading.textContent ?? "");
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
describe("the base sections (AC-8)", () => {
|
|
184
|
-
it("renders General, Security, Notifications and Privacy — and no admin-only section", async () => {
|
|
185
|
-
await renderPage();
|
|
186
|
-
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Settings");
|
|
187
|
-
expect(sectionTitles()).toEqual(["General", "Security", "Notifications", "Privacy"]);
|
|
188
|
-
for (const absent of [/credentials/i, /scope/i, /organizations/i, /credits/i]) {
|
|
189
|
-
expect(screen.queryByRole("heading", { name: absent })).not.toBeInTheDocument();
|
|
190
|
-
}
|
|
32
|
+
describe("AC-6 — /account/settings redirects to /account/general (D8)", () => {
|
|
33
|
+
it("redirects the default locale to /account/general", async () => {
|
|
34
|
+
await Page({ params: Promise.resolve({ locale: "en" }) });
|
|
35
|
+
expect(mockRedirect).toHaveBeenCalledTimes(1);
|
|
36
|
+
expect(mockRedirect).toHaveBeenCalledWith("/account/general");
|
|
191
37
|
});
|
|
192
38
|
|
|
193
|
-
it("
|
|
194
|
-
await
|
|
195
|
-
|
|
196
|
-
expect(document.getElementById(id)).toBeInTheDocument();
|
|
197
|
-
}
|
|
39
|
+
it("keeps a non-default locale", async () => {
|
|
40
|
+
await Page({ params: Promise.resolve({ locale: "fr" }) });
|
|
41
|
+
expect(mockRedirect).toHaveBeenCalledWith("/fr/account/general");
|
|
198
42
|
});
|
|
199
43
|
});
|
|
200
44
|
|
|
201
|
-
describe("
|
|
202
|
-
it(
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
expect(within(billing).queryByTestId("account-billing-manage")).not.toBeInTheDocument();
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it("renders for portalAvailable: true, with Manage billing", async () => {
|
|
212
|
-
mockSubscriptionMe.mockResolvedValue(subscriptionBody({ portalAvailable: true }));
|
|
213
|
-
await renderPage();
|
|
214
|
-
const billing = document.getElementById("billing")!;
|
|
215
|
-
expect(within(billing).getByTestId("account-billing-manage")).toHaveTextContent(
|
|
216
|
-
"Manage billing",
|
|
217
|
-
);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it("shows when access ends, when set", async () => {
|
|
221
|
-
mockSubscriptionMe.mockResolvedValue(
|
|
222
|
-
subscriptionBody({ status: "canceled", accessRevokesAt: "2026-10-20T00:00:00.000Z" }),
|
|
223
|
-
);
|
|
224
|
-
await renderPage();
|
|
225
|
-
const billing = document.getElementById("billing")!;
|
|
226
|
-
expect(billing).toHaveTextContent("Canceled");
|
|
227
|
-
expect(billing).toHaveTextContent("Access ends");
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it("is absent for status: null with portalAvailable: false", async () => {
|
|
231
|
-
await renderPage();
|
|
232
|
-
expect(document.getElementById("billing")).not.toBeInTheDocument();
|
|
233
|
-
expect(sectionTitles()).not.toContain("Billing");
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it("is absent for a 404 (the commerce-disabled build)", async () => {
|
|
237
|
-
mockSubscriptionMe.mockResolvedValue({ ok: false, status: 404, body: {} });
|
|
238
|
-
await renderPage();
|
|
239
|
-
expect(document.getElementById("billing")).not.toBeInTheDocument();
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it.each([401, 403])("is absent for a %i", async (status) => {
|
|
243
|
-
mockSubscriptionMe.mockResolvedValue({ ok: false, status, body: {} });
|
|
244
|
-
await renderPage();
|
|
245
|
-
expect(document.getElementById("billing")).not.toBeInTheDocument();
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
describe("General — the member's name (operator fold-in, #7418)", () => {
|
|
250
|
-
it("shows the email read-only and the name in an editable field", async () => {
|
|
251
|
-
await renderPage();
|
|
252
|
-
const general = document.getElementById("general")!;
|
|
253
|
-
expect(within(general).getByTestId("account-settings-email")).toHaveTextContent(
|
|
254
|
-
"ada@example.com",
|
|
255
|
-
);
|
|
256
|
-
expect(within(general).getByLabelText("Name")).toHaveValue("Ada Lovelace");
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it("a member with no name can set one: Save is disabled until the field changes, then saves through updateUser", async () => {
|
|
260
|
-
mockGetSession.mockResolvedValue({ user: { ...SESSION.user, name: "" } });
|
|
261
|
-
await renderPage();
|
|
262
|
-
const input = screen.getByLabelText("Name");
|
|
263
|
-
expect(input).toHaveValue("");
|
|
264
|
-
const save = screen.getByTestId("account-name-save");
|
|
265
|
-
expect(save).toBeDisabled();
|
|
266
|
-
fireEvent.change(input, { target: { value: " Grace Hopper " } });
|
|
267
|
-
expect(save).toBeEnabled();
|
|
268
|
-
expect(screen.getByTestId("account-name-status")).toHaveTextContent("Unsaved changes");
|
|
269
|
-
await act(async () => {
|
|
270
|
-
fireEvent.click(save);
|
|
271
|
-
});
|
|
272
|
-
expect(mockUpdateUser).toHaveBeenCalledWith({ name: "Grace Hopper" });
|
|
273
|
-
await waitFor(() => expect(mockRefresh).toHaveBeenCalledTimes(1));
|
|
274
|
-
expect(screen.getByTestId("account-name-status")).toHaveTextContent("Saved.");
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it("a blank name is refused without a call", async () => {
|
|
278
|
-
await renderPage();
|
|
279
|
-
const input = screen.getByLabelText("Name");
|
|
280
|
-
fireEvent.change(input, { target: { value: " " } });
|
|
281
|
-
await act(async () => {
|
|
282
|
-
fireEvent.click(screen.getByTestId("account-name-save"));
|
|
283
|
-
});
|
|
284
|
-
expect(mockUpdateUser).not.toHaveBeenCalled();
|
|
285
|
-
expect(screen.getByRole("alert")).toHaveTextContent("Enter your name.");
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
it("a failed save says so and keeps the edit", async () => {
|
|
289
|
-
mockUpdateUser.mockResolvedValue({ data: null, error: { message: "nope" } });
|
|
290
|
-
await renderPage();
|
|
291
|
-
const input = screen.getByLabelText("Name");
|
|
292
|
-
fireEvent.change(input, { target: { value: "Ada King" } });
|
|
293
|
-
await act(async () => {
|
|
294
|
-
fireEvent.click(screen.getByTestId("account-name-save"));
|
|
295
|
-
});
|
|
296
|
-
expect(screen.getByRole("alert")).toHaveTextContent("We couldn't save your name.");
|
|
297
|
-
expect(input).toHaveValue("Ada King");
|
|
298
|
-
expect(mockRefresh).not.toHaveBeenCalled();
|
|
299
|
-
});
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
describe("General — a save that throws", () => {
|
|
303
|
-
it("a network failure says so and re-enables Save", async () => {
|
|
304
|
-
mockUpdateUser.mockRejectedValue(new TypeError("Failed to fetch"));
|
|
305
|
-
await renderPage();
|
|
306
|
-
fireEvent.change(screen.getByLabelText("Name"), { target: { value: "Ada King" } });
|
|
307
|
-
await act(async () => {
|
|
308
|
-
fireEvent.click(screen.getByTestId("account-name-save"));
|
|
309
|
-
});
|
|
310
|
-
expect(screen.getByRole("alert")).toHaveTextContent("We couldn't save your name.");
|
|
311
|
-
expect(screen.getByTestId("account-name-save")).toBeEnabled();
|
|
312
|
-
});
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
describe("General — color mode only on a multi-mode theme (operator fold-in)", () => {
|
|
316
|
-
it("a site with no theme block (the default light + dark palette) shows Light · Auto · Dark", async () => {
|
|
317
|
-
await renderPage();
|
|
318
|
-
const control = screen.getByTestId("account-color-mode");
|
|
319
|
-
expect(within(control).getAllByRole("radio").map((r) => r.textContent)).toEqual([
|
|
320
|
-
"Light",
|
|
321
|
-
"Auto",
|
|
322
|
-
"Dark",
|
|
45
|
+
describe("the one list of User settings' routes (#8441)", () => {
|
|
46
|
+
it("names the four tabs, in the reference's order", () => {
|
|
47
|
+
expect(SETTINGS_ROUTES.map(({ key, path }) => [key, path])).toEqual([
|
|
48
|
+
["general", "/account/general"],
|
|
49
|
+
["security", "/account/security"],
|
|
50
|
+
["notifications", "/account/notifications"],
|
|
51
|
+
["billing", "/account/billing"],
|
|
323
52
|
]);
|
|
324
53
|
});
|
|
325
54
|
|
|
326
|
-
it("
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
expect(
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
expect(screen.queryByTestId("account-color-mode")).not.toBeInTheDocument();
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
it("choosing Dark writes the theme cookie and the root's data-theme", async () => {
|
|
339
|
-
await renderPage();
|
|
340
|
-
const control = screen.getByTestId("account-color-mode");
|
|
341
|
-
fireEvent.click(within(control).getByRole("radio", { name: "Dark" }));
|
|
342
|
-
expect(document.documentElement.getAttribute("data-theme")).toBe("dark");
|
|
343
|
-
expect(document.cookie).toContain("ailk-theme=dark");
|
|
344
|
-
});
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
describe("Security — the configured sign-in methods (D10)", () => {
|
|
348
|
-
it("lists each configured method; no Password row without the password method", async () => {
|
|
349
|
-
process.env.GOOGLE_CLIENT_ID = "id";
|
|
350
|
-
process.env.GOOGLE_CLIENT_SECRET = "secret";
|
|
351
|
-
await renderPage();
|
|
352
|
-
const security = document.getElementById("security")!;
|
|
353
|
-
expect(within(security).getByText("Google")).toBeInTheDocument();
|
|
354
|
-
expect(within(security).queryByText("GitHub")).not.toBeInTheDocument();
|
|
355
|
-
expect(within(security).queryByText("Password")).not.toBeInTheDocument();
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
it("the password method adds a Password row", async () => {
|
|
359
|
-
process.env.AUTH_EMAIL_SERVER = "smtp://x";
|
|
360
|
-
process.env.AUTH_EMAIL_FROM = "site@example.com";
|
|
361
|
-
await renderPage();
|
|
362
|
-
const security = document.getElementById("security")!;
|
|
363
|
-
expect(within(security).getByText("Password")).toBeInTheDocument();
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
it("no configured method says so", async () => {
|
|
367
|
-
await renderPage();
|
|
368
|
-
expect(document.getElementById("security")).toHaveTextContent(
|
|
369
|
-
"No sign-in method is configured.",
|
|
55
|
+
it.each(["en", "fr"])("the settings frame's tabs are that list, in %s", (locale) => {
|
|
56
|
+
const t: Translate = (key) => key;
|
|
57
|
+
const { tabs } = memberSettingsFrame(locale, t, "security");
|
|
58
|
+
expect(tabs.map(({ key, href, label }) => [key, href, label])).toEqual(
|
|
59
|
+
SETTINGS_ROUTES.map(({ key, path, titleKey }) => [
|
|
60
|
+
key,
|
|
61
|
+
locale === "en" ? path : `/${locale}${path}`,
|
|
62
|
+
titleKey,
|
|
63
|
+
]),
|
|
370
64
|
);
|
|
371
65
|
});
|
|
372
66
|
});
|
|
373
67
|
|
|
374
|
-
describe("
|
|
375
|
-
it("
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
subject: "Account deletion request",
|
|
398
|
-
message: "Please delete my account and its data.",
|
|
399
|
-
},
|
|
400
|
-
});
|
|
401
|
-
await waitFor(() =>
|
|
402
|
-
expect(within(privacy).getByRole("status")).toHaveTextContent(
|
|
403
|
-
"Your deletion request is on its way.",
|
|
404
|
-
),
|
|
405
|
-
);
|
|
68
|
+
describe("isSettingsPath — an exact, locale-aware match (#8441)", () => {
|
|
69
|
+
it.each(SETTINGS_ROUTES.map(({ path }) => path))("%s is a settings route, bare or prefixed", (path) => {
|
|
70
|
+
expect(isSettingsPath(path, "en")).toBe(true);
|
|
71
|
+
expect(isSettingsPath(`/fr${path}`, "fr")).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it.each([
|
|
75
|
+
["/account", "en"],
|
|
76
|
+
["/account/settings", "en"],
|
|
77
|
+
["/account/support", "en"],
|
|
78
|
+
["/account/artifacts", "en"],
|
|
79
|
+
// A sub-route stays inside the shell unless it is listed.
|
|
80
|
+
["/account/billing/history", "en"],
|
|
81
|
+
["/fr/account/billing/history", "fr"],
|
|
82
|
+
["/account/general/", "en"],
|
|
83
|
+
["/account/generalx", "en"],
|
|
84
|
+
// Only the request's own locale is a prefix.
|
|
85
|
+
["/fr/account/general", "en"],
|
|
86
|
+
["/de/account/general", "fr"],
|
|
87
|
+
["/frx/account/general", "fr"],
|
|
88
|
+
["", "en"],
|
|
89
|
+
])("%s (locale %s) is not", (path, locale) => {
|
|
90
|
+
expect(isSettingsPath(path, locale)).toBe(false);
|
|
406
91
|
});
|
|
407
92
|
});
|
|
@@ -189,15 +189,20 @@ describe("submitSupportRequest — the Server Action's own guards", () => {
|
|
|
189
189
|
expect(mockLeadsSubmit).not.toHaveBeenCalled();
|
|
190
190
|
});
|
|
191
191
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
192
|
+
// #8232 D12 — a deletion request goes through `member.requestDeletion`
|
|
193
|
+
// (`profile-actions.ts`), so Support files support only.
|
|
194
|
+
it.each(["refund", "deletion"])(
|
|
195
|
+
"every request files as support, a posted kind=%s included; the email is always the session's",
|
|
196
|
+
async (kind) => {
|
|
197
|
+
await submitSupportRequest(
|
|
198
|
+
{ kind: "idle" },
|
|
199
|
+
form({ kind, subject: "Hi", message: "Help", email: "someone@else.com" }),
|
|
200
|
+
);
|
|
201
|
+
expect(mockLeadsSubmit).toHaveBeenCalledWith({
|
|
202
|
+
source: "web",
|
|
203
|
+
email: "ada@example.com",
|
|
204
|
+
payload: { kind: "support", subject: "Hi", message: "Help" },
|
|
205
|
+
});
|
|
206
|
+
},
|
|
207
|
+
);
|
|
203
208
|
});
|
|
@@ -150,7 +150,7 @@ function addNameItem(session: AccountSession, now: string): ForYouItem[] {
|
|
|
150
150
|
},
|
|
151
151
|
title: ctx.t("forYou.addName"),
|
|
152
152
|
time: { kind: "waiting", at: session.createdAt ?? now },
|
|
153
|
-
action: { kind: "open", href: localeHref(ctx.locale, "/account/
|
|
153
|
+
action: { kind: "open", href: localeHref(ctx.locale, "/account/general") },
|
|
154
154
|
dismissible: true,
|
|
155
155
|
},
|
|
156
156
|
];
|