create-ailk 0.17.0 → 0.19.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 +6 -5
- package/NOTICE +2 -0
- package/dist/add-module.js +4 -1
- 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/lib/env-catalog.js +5 -9
- 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 +9 -0
- package/dist/lib/scaffold-render-sweep.js +24 -3
- package/dist/license-set.d.ts +1 -1
- package/dist/license-set.js +13 -8
- package/dist/module-architecture.js +119 -0
- package/dist/programmatic.js +3 -0
- 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.js +140 -1
- package/dist/template-manifest.d.ts +4 -3
- package/dist/topology.d.ts +15 -2
- package/dist/topology.js +83 -4
- package/package.json +5 -4
- package/templates/.env.example +3 -3
- package/templates/.github/workflows/ci.yml +5 -0
- package/templates/apps/api/CLAUDE.md +25 -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/lib/__mocks__/prisma.ts +9 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +38 -0
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +5 -4
- package/templates/apps/api/src/lib/stripe.ts +81 -0
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +266 -39
- package/templates/apps/api/src/middleware/auth.ts +206 -46
- package/templates/apps/api/src/routes/leads/index.ts +4 -0
- package/templates/apps/api/src/server.ts +107 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +58 -0
- package/templates/apps/api/src/services/lead-routing.ts +30 -2
- package/templates/apps/api/vercel.json +2 -2
- package/templates/apps/mcp/.env.example +10 -0
- package/templates/apps/mcp/CLAUDE.md +58 -7
- 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__/app-entry.test.ts +325 -0
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +15 -3
- package/templates/apps/mcp/__tests__/boot.test.ts +280 -0
- package/templates/apps/mcp/__tests__/http-transport.test.ts +291 -2
- package/templates/apps/mcp/__tests__/index.test.ts +86 -47
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +5 -6
- package/templates/apps/mcp/__tests__/lib/content-root.test.ts +93 -0
- package/templates/apps/mcp/__tests__/request_callback.test.ts +69 -7
- package/templates/apps/mcp/__tests__/server.test.ts +63 -6
- 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/app.ts +32 -0
- package/templates/apps/mcp/src/boot.ts +167 -0
- package/templates/apps/mcp/src/http-transport.ts +132 -88
- package/templates/apps/mcp/src/index.ts +19 -126
- package/templates/apps/mcp/src/lib/audit-single-page.ts +7 -6
- package/templates/apps/mcp/src/lib/content-adapter.ts +34 -5
- package/templates/apps/mcp/src/lib/topology.generated.ts +3 -0
- package/templates/apps/mcp/src/request-context.ts +52 -0
- package/templates/apps/mcp/src/server.ts +10 -2
- package/templates/apps/mcp/src/serverless-app.ts +88 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +9 -1
- package/templates/apps/mcp/src/tools/get_business_hours.ts +6 -5
- package/templates/apps/mcp/src/tools/get_faq.ts +6 -5
- package/templates/apps/mcp/src/tools/get_locations.ts +6 -5
- package/templates/apps/mcp/src/tools/get_offerings.ts +6 -5
- package/templates/apps/mcp/src/tools/get_pricing.ts +6 -5
- package/templates/apps/mcp/src/tools/get_site_info.ts +6 -5
- package/templates/apps/mcp/src/tools/list_pages.ts +8 -7
- package/templates/apps/mcp/src/tools/read_content.ts +7 -7
- package/templates/apps/mcp/src/tools/request_callback.ts +9 -1
- package/templates/apps/mcp/src/tools/search_content.ts +6 -5
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +9 -1
- package/templates/apps/mcp/tsconfig.json +2 -0
- 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/.well-known/mcp.json/build-manifest.ts +16 -4
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +3 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +288 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +568 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +272 -42
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +11 -13
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +115 -16
- 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 +126 -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 +139 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +20 -390
- 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 +114 -50
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +25 -32
- 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 +177 -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/deletion-request.tsx +132 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +131 -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 +4 -1
- 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 +80 -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 +108 -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 +5 -1
- package/templates/apps/web/lib/__tests__/account-config.test.ts +258 -46
- package/templates/apps/web/lib/__tests__/site-brand.test.ts +42 -0
- package/templates/apps/web/lib/account-config.ts +188 -52
- 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/site-brand.tsx +22 -6
- package/templates/apps/web/lib/topology.generated.ts +27 -0
- package/templates/apps/web/messages/en.json +156 -35
- package/templates/database/CHANGELOG.md +21 -0
- package/templates/database/LICENSE +6 -5
- package/templates/database/NOTICE +2 -0
- package/templates/database/README.md +1 -1
- 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__/seed.test.ts +54 -2
- package/templates/database/inbox/schema.prisma +90 -15
- 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/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,37 @@
|
|
|
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
|
-
* 3. General lets a member SET and edit their name (an accepted applicant
|
|
12
|
-
* arrives with none, #7418) — saved through Better Auth's `updateUser`.
|
|
13
|
-
* 4. The color-mode control renders only when the site's theme defines
|
|
14
|
-
* more than one mode.
|
|
15
|
-
* 5. Security lists the configured sign-in methods; a Password row only
|
|
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).
|
|
18
12
|
*/
|
|
19
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
20
|
-
import { tmpdir } from "node:os";
|
|
21
|
-
import { join } from "node:path";
|
|
22
13
|
|
|
23
|
-
|
|
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
|
-
|
|
84
|
-
const mockRefresh = jest.fn();
|
|
14
|
+
const mockRedirect = jest.fn();
|
|
85
15
|
jest.mock("next/navigation", () => ({
|
|
86
16
|
__esModule: true,
|
|
87
|
-
|
|
88
|
-
usePathname: () => "/account/settings",
|
|
89
|
-
redirect: jest.fn(),
|
|
17
|
+
redirect: (...args: unknown[]) => mockRedirect(...args),
|
|
90
18
|
}));
|
|
91
19
|
|
|
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
20
|
import Page from "../settings/page";
|
|
103
21
|
|
|
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
|
-
});
|
|
155
|
-
|
|
156
22
|
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();
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
afterEach(() => {
|
|
169
|
-
rmSync(contentRoot, { recursive: true, force: true });
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
async function renderPage(): Promise<void> {
|
|
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
|
-
}
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it("each section is an anchor the For-you items link to", async () => {
|
|
194
|
-
await renderPage();
|
|
195
|
-
for (const id of ["general", "security", "notifications", "privacy"]) {
|
|
196
|
-
expect(document.getElementById(id)).toBeInTheDocument();
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
describe("Billing (AC-8, D10)", () => {
|
|
202
|
-
it('renders for status: "active", with the status', async () => {
|
|
203
|
-
mockSubscriptionMe.mockResolvedValue(subscriptionBody({ status: "active" }));
|
|
204
|
-
await renderPage();
|
|
205
|
-
const billing = document.getElementById("billing")!;
|
|
206
|
-
expect(within(billing).getByRole("heading", { level: 2 })).toHaveTextContent("Billing");
|
|
207
|
-
expect(billing).toHaveTextContent("Active");
|
|
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",
|
|
323
|
-
]);
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
it("a theme declaring both modes shows the control", async () => {
|
|
327
|
-
writeSite('theme:\n colors:\n light:\n bg: "#ffffff"\n dark:\n bg: "#000000"');
|
|
328
|
-
await renderPage();
|
|
329
|
-
expect(screen.getByTestId("account-color-mode")).toBeInTheDocument();
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
it("a light-only theme shows no control", async () => {
|
|
333
|
-
writeSite('theme:\n colors:\n light:\n bg: "#fdfcfa"\n text: "#1c1917"');
|
|
334
|
-
await renderPage();
|
|
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.",
|
|
370
|
-
);
|
|
371
|
-
});
|
|
23
|
+
mockRedirect.mockReset();
|
|
372
24
|
});
|
|
373
25
|
|
|
374
|
-
describe("
|
|
375
|
-
it("
|
|
376
|
-
await
|
|
377
|
-
|
|
378
|
-
expect(
|
|
379
|
-
"href",
|
|
380
|
-
"/privacy#cookie-settings",
|
|
381
|
-
);
|
|
26
|
+
describe("AC-6 — /account/settings redirects to /account/general (D8)", () => {
|
|
27
|
+
it("redirects the default locale to /account/general", async () => {
|
|
28
|
+
await Page({ params: Promise.resolve({ locale: "en" }) });
|
|
29
|
+
expect(mockRedirect).toHaveBeenCalledTimes(1);
|
|
30
|
+
expect(mockRedirect).toHaveBeenCalledWith("/account/general");
|
|
382
31
|
});
|
|
383
32
|
|
|
384
|
-
it("
|
|
385
|
-
await
|
|
386
|
-
|
|
387
|
-
await act(async () => {
|
|
388
|
-
fireEvent.click(within(privacy).getByTestId("account-deletion-request"));
|
|
389
|
-
});
|
|
390
|
-
await waitFor(() => expect(mockLeadsSubmit).toHaveBeenCalledTimes(1));
|
|
391
|
-
const [body] = mockLeadsSubmit.mock.calls[0] as [Record<string, unknown>];
|
|
392
|
-
expect(body).toEqual({
|
|
393
|
-
source: "web",
|
|
394
|
-
email: "ada@example.com",
|
|
395
|
-
payload: {
|
|
396
|
-
kind: "deletion",
|
|
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
|
-
);
|
|
33
|
+
it("keeps a non-default locale", async () => {
|
|
34
|
+
await Page({ params: Promise.resolve({ locale: "fr" }) });
|
|
35
|
+
expect(mockRedirect).toHaveBeenCalledWith("/fr/account/general");
|
|
406
36
|
});
|
|
407
37
|
});
|
|
@@ -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
|
];
|