create-ailk 0.18.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/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 +7 -2
- package/dist/topology.js +60 -3
- 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 +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__/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 +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 +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/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/topology.generated.ts +24 -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
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for /[locale]/account/billing — User settings › Billing (#8232, spec
|
|
3
|
+
* docs/specs/8232-member-settings-account-general-and-spec.md D1, D9; AC-2).
|
|
4
|
+
* The single-site render: `subscription.me` is the mocked boundary.
|
|
5
|
+
*
|
|
6
|
+
* Covers:
|
|
7
|
+
* (a) a subscription shows the plan, the status, the card and Manage in
|
|
8
|
+
* Stripe; (b) no Manage without a portal; (c) `status: null` and each
|
|
9
|
+
* of 401, 403 and 404 say plainly there is no subscription, with no
|
|
10
|
+
* button; (d) a 500 or a throw shows the error row, never "no
|
|
11
|
+
* subscription"; (e) a contributed billing section renders after every
|
|
12
|
+
* one of these; (f) the read asks for `include: "billing"`.
|
|
13
|
+
* #8217 AC-1 (spec docs/specs/8217-spoke-billing-spoke-checkout-reads-spec.md
|
|
14
|
+
* D10, D11): `openBillingPortal()` mints the portal through
|
|
15
|
+
* `billingPortalUrl()` — the hub's route, once, with the cookie init — and
|
|
16
|
+
* redirects to it; a non-2xx or a throw redirects nowhere. The single site
|
|
17
|
+
* never shows the unreadable line.
|
|
18
|
+
*/
|
|
19
|
+
import { render, screen, within } from "@testing-library/react";
|
|
20
|
+
|
|
21
|
+
jest.mock("next-intl/server", () => {
|
|
22
|
+
const msgs: unknown = require("../../../../../messages/en.json");
|
|
23
|
+
const dig = (obj: unknown, path: string): unknown =>
|
|
24
|
+
path.split(".").reduce<unknown>(
|
|
25
|
+
(acc, k) => (acc !== null && typeof acc === "object" ? (acc as Record<string, unknown>)[k] : undefined),
|
|
26
|
+
obj,
|
|
27
|
+
);
|
|
28
|
+
return {
|
|
29
|
+
__esModule: true,
|
|
30
|
+
getTranslations: async (arg: string | { namespace?: string; locale?: string }) => {
|
|
31
|
+
const ns = typeof arg === "string" ? arg : (arg.namespace ?? "");
|
|
32
|
+
const messages = dig(msgs, ns);
|
|
33
|
+
return (key: string, values?: Record<string, unknown>): string => {
|
|
34
|
+
const value = dig(messages, key);
|
|
35
|
+
if (typeof value !== "string") return `${ns}.${key}`;
|
|
36
|
+
return value.replace(/\{(\w+)\}/g, (m, name: string) =>
|
|
37
|
+
values && name in values ? String(values[name]) : m,
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
jest.mock("next/headers", () => ({
|
|
45
|
+
__esModule: true,
|
|
46
|
+
headers: jest.fn().mockResolvedValue(new Headers()),
|
|
47
|
+
cookies: jest.fn().mockResolvedValue({ toString: () => "session=abc" }),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
const mockGetSession = jest.fn();
|
|
51
|
+
// 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.
|
|
52
|
+
jest.mock("../../../../../auth", () => ({
|
|
53
|
+
__esModule: true,
|
|
54
|
+
auth: { api: { getSession: (...args: unknown[]) => mockGetSession(...args) } },
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
const mockSubscriptionMe = jest.fn();
|
|
58
|
+
const mockPortalSession = jest.fn();
|
|
59
|
+
jest.mock("@working-theory/api-client", () => ({
|
|
60
|
+
__esModule: true,
|
|
61
|
+
waitlistSignups: { me: jest.fn() },
|
|
62
|
+
flowCheckouts: { me: jest.fn() },
|
|
63
|
+
subscription: {
|
|
64
|
+
me: (...args: unknown[]) => mockSubscriptionMe(...args),
|
|
65
|
+
portalSession: (...args: unknown[]) => mockPortalSession(...args),
|
|
66
|
+
},
|
|
67
|
+
leads: { submit: jest.fn() },
|
|
68
|
+
member: {},
|
|
69
|
+
hubSignIn: { me: jest.fn() },
|
|
70
|
+
visitorForwardHeaders: () => null,
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
const mockRedirect = jest.fn();
|
|
74
|
+
jest.mock("next/navigation", () => ({
|
|
75
|
+
__esModule: true,
|
|
76
|
+
useRouter: () => ({ push: jest.fn(), refresh: jest.fn(), replace: jest.fn() }),
|
|
77
|
+
usePathname: () => "/account/billing",
|
|
78
|
+
redirect: (...args: unknown[]) => mockRedirect(...args),
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
import type { AccountBillingSection } from "@working-theory/templates/account";
|
|
82
|
+
|
|
83
|
+
import { accountBillingSections } from "../../../../../lib/account-config";
|
|
84
|
+
import { openBillingPortal } from "../actions";
|
|
85
|
+
|
|
86
|
+
// The Billing route's own page, loaded with `import()`: architecture.yaml bans
|
|
87
|
+
// a static import of a relative billing/ path to keep OSS code out of the
|
|
88
|
+
// apps-paid/billing service's source, and this route directory shares the name.
|
|
89
|
+
const loadPage = async () => (await import("../billing/page")).default;
|
|
90
|
+
|
|
91
|
+
const SESSION = {
|
|
92
|
+
user: {
|
|
93
|
+
id: "user_1",
|
|
94
|
+
name: "Ada Lovelace",
|
|
95
|
+
email: "ada@example.com",
|
|
96
|
+
emailVerified: true,
|
|
97
|
+
createdAt: new Date("2026-09-01T00:00:00.000Z"),
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const ACTIVE = {
|
|
102
|
+
actionRequired: false,
|
|
103
|
+
status: "active",
|
|
104
|
+
accessRevokesAt: null,
|
|
105
|
+
portalAvailable: true,
|
|
106
|
+
planName: "Pro",
|
|
107
|
+
paymentMethod: { brand: "visa", last4: "4242", expMonth: 4, expYear: 2027 },
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const answer = (body: Record<string, unknown>) => ({ ok: true, body: { ...ACTIVE, ...body } });
|
|
111
|
+
|
|
112
|
+
beforeEach(() => {
|
|
113
|
+
mockGetSession.mockReset().mockResolvedValue(SESSION);
|
|
114
|
+
mockSubscriptionMe.mockReset().mockResolvedValue(answer({}));
|
|
115
|
+
mockPortalSession.mockReset();
|
|
116
|
+
mockRedirect.mockReset();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
async function renderPage(locale = "en"): Promise<void> {
|
|
120
|
+
const Page = await loadPage();
|
|
121
|
+
render(await Page({ params: Promise.resolve({ locale }) }));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
describe("the frame (D7)", () => {
|
|
125
|
+
it("renders in the settings frame with Billing current", async () => {
|
|
126
|
+
await renderPage();
|
|
127
|
+
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Billing");
|
|
128
|
+
expect(screen.getByTestId("account-settings-back")).toHaveAttribute("href", "/account");
|
|
129
|
+
expect(screen.getByTestId("account-settings-rail-billing")).toHaveAttribute("aria-current", "page");
|
|
130
|
+
expect(screen.getByTestId("account-settings-rail-general")).not.toHaveAttribute("aria-current");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("AC-5 — Billing as the current-plan card (D8)", () => {
|
|
135
|
+
it("(a) shows the plan, the status, the card and Manage in Stripe under the Current plan heading", async () => {
|
|
136
|
+
await renderPage();
|
|
137
|
+
expect(screen.getByText("Current plan")).toBeInTheDocument();
|
|
138
|
+
const card = screen.getByTestId("account-billing-card");
|
|
139
|
+
expect(within(card).getByTestId("account-billing-plan")).toHaveTextContent("Pro");
|
|
140
|
+
expect(within(card).getByTestId("account-billing-status")).toHaveTextContent("Active");
|
|
141
|
+
expect(within(card).getByTestId("account-billing-payment")).toHaveTextContent(
|
|
142
|
+
"visa ending in 4242, expires 04/2027",
|
|
143
|
+
);
|
|
144
|
+
expect(screen.getByTestId("account-billing-manage")).toHaveTextContent("Manage in Stripe");
|
|
145
|
+
expect(screen.queryByTestId("account-billing-none")).not.toBeInTheDocument();
|
|
146
|
+
expect(screen.queryByTestId("account-billing-unreadable")).not.toBeInTheDocument();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("(a) no plan name reads as 'Your subscription', and no card as 'No card on file'", async () => {
|
|
150
|
+
mockSubscriptionMe.mockResolvedValue(
|
|
151
|
+
answer({ status: "past_due", planName: null, paymentMethod: null }),
|
|
152
|
+
);
|
|
153
|
+
await renderPage();
|
|
154
|
+
expect(screen.getByTestId("account-billing-plan")).toHaveTextContent("Your subscription");
|
|
155
|
+
expect(screen.getByTestId("account-billing-status")).toHaveTextContent("Payment due");
|
|
156
|
+
expect(screen.getByTestId("account-billing-payment")).toHaveTextContent("No card on file");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("(a) an older API without the billing fields still renders the status", async () => {
|
|
160
|
+
mockSubscriptionMe.mockResolvedValue({
|
|
161
|
+
ok: true,
|
|
162
|
+
body: { actionRequired: false, status: "trialing", accessRevokesAt: null, portalAvailable: false },
|
|
163
|
+
});
|
|
164
|
+
await renderPage();
|
|
165
|
+
expect(screen.getByTestId("account-billing-status")).toHaveTextContent("Trial");
|
|
166
|
+
expect(screen.getByTestId("account-billing-payment")).toHaveTextContent("No card on file");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("(a) appends when access ends, when set", async () => {
|
|
170
|
+
mockSubscriptionMe.mockResolvedValue(
|
|
171
|
+
answer({ status: "canceled", accessRevokesAt: "2026-10-20T00:00:00.000Z" }),
|
|
172
|
+
);
|
|
173
|
+
await renderPage();
|
|
174
|
+
const status = screen.getByTestId("account-billing-status");
|
|
175
|
+
expect(status).toHaveTextContent("Canceled");
|
|
176
|
+
expect(status).toHaveTextContent("Access ends");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("(b) no portal, no Manage button", async () => {
|
|
180
|
+
mockSubscriptionMe.mockResolvedValue(answer({ portalAvailable: false }));
|
|
181
|
+
await renderPage();
|
|
182
|
+
expect(screen.getByTestId("account-billing-card")).toBeInTheDocument();
|
|
183
|
+
expect(screen.queryByTestId("account-billing-manage")).not.toBeInTheDocument();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("(c) status: null renders the same card shape with the plain line and no button — even with a portal", async () => {
|
|
187
|
+
mockSubscriptionMe.mockResolvedValue(answer({ status: null, portalAvailable: true }));
|
|
188
|
+
await renderPage();
|
|
189
|
+
expect(screen.getByTestId("account-billing-none")).toHaveTextContent("You don't have a subscription.");
|
|
190
|
+
expect(screen.queryByTestId("account-billing-manage")).not.toBeInTheDocument();
|
|
191
|
+
expect(screen.queryByTestId("account-billing-plan")).not.toBeInTheDocument();
|
|
192
|
+
expect(screen.queryByTestId("account-billing-status")).not.toBeInTheDocument();
|
|
193
|
+
expect(screen.queryByTestId("account-billing-payment")).not.toBeInTheDocument();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it.each([401, 403, 404])("(c) a %i says there is no subscription, with no button", async (status) => {
|
|
197
|
+
mockSubscriptionMe.mockResolvedValue({ ok: false, status, body: {} });
|
|
198
|
+
await renderPage();
|
|
199
|
+
expect(screen.getByTestId("account-billing-none")).toBeInTheDocument();
|
|
200
|
+
expect(screen.queryByTestId("account-billing-manage")).not.toBeInTheDocument();
|
|
201
|
+
expect(screen.queryByTestId("inbox-error")).not.toBeInTheDocument();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("(g) through D6, the member card renders the shared CurrentPlanCard — exactly one card", async () => {
|
|
205
|
+
await renderPage();
|
|
206
|
+
expect(screen.getAllByTestId("account-billing-card")).toHaveLength(1);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("(d) a 500 shows the error row with Retry, not 'no subscription'", async () => {
|
|
210
|
+
mockSubscriptionMe.mockResolvedValue({ ok: false, status: 500, body: {} });
|
|
211
|
+
await renderPage();
|
|
212
|
+
expect(screen.getByTestId("inbox-error")).toHaveTextContent("We couldn't load your billing details.");
|
|
213
|
+
expect(screen.getByTestId("inbox-retry")).toBeInTheDocument();
|
|
214
|
+
expect(screen.queryByTestId("account-billing-none")).not.toBeInTheDocument();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("(d) a throw shows the error row, not 'no subscription'", async () => {
|
|
218
|
+
mockSubscriptionMe.mockRejectedValue(new TypeError("fetch failed"));
|
|
219
|
+
await renderPage();
|
|
220
|
+
expect(screen.getByTestId("inbox-error")).toBeInTheDocument();
|
|
221
|
+
expect(screen.queryByTestId("account-billing-none")).not.toBeInTheDocument();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("(f) the read asks for the billing fields with the caller's cookie", async () => {
|
|
225
|
+
await renderPage();
|
|
226
|
+
expect(mockSubscriptionMe).toHaveBeenCalledWith(
|
|
227
|
+
{ headers: { cookie: "session=abc" } },
|
|
228
|
+
{ include: "billing" },
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("AC-2 (e) — contributed billing sections render after the base content", () => {
|
|
234
|
+
const section: AccountBillingSection = {
|
|
235
|
+
id: "licence",
|
|
236
|
+
title: "settings.billing.title",
|
|
237
|
+
render: () => <p data-testid="licence-body">Licence</p>,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
beforeAll(() => {
|
|
241
|
+
accountBillingSections.push(section);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
afterAll(() => {
|
|
245
|
+
accountBillingSections.splice(accountBillingSections.indexOf(section), 1);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const states: [string, () => void, string][] = [
|
|
249
|
+
["a subscription", () => undefined, "account-billing-card"],
|
|
250
|
+
["no subscription", () => mockSubscriptionMe.mockResolvedValue(answer({ status: null })), "account-billing-card"],
|
|
251
|
+
["a 404", () => mockSubscriptionMe.mockResolvedValue({ ok: false, status: 404, body: {} }), "account-billing-card"],
|
|
252
|
+
["a failed read", () => mockSubscriptionMe.mockResolvedValue({ ok: false, status: 500, body: {} }), "inbox-error"],
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
it.each(states)("after %s", async (_label, arrange, baseTestId) => {
|
|
256
|
+
arrange();
|
|
257
|
+
await renderPage();
|
|
258
|
+
const base = screen.getByTestId(baseTestId);
|
|
259
|
+
const contributed = screen.getByTestId("account-settings-licence");
|
|
260
|
+
expect(within(contributed).getByTestId("licence-body")).toBeInTheDocument();
|
|
261
|
+
expect(base.compareDocumentPosition(contributed) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe("#8217 AC-1 — the single site's Manage in Stripe (D10, D11)", () => {
|
|
266
|
+
const PORTAL_URL = "https://billing.stripe.com/p/session/test_single_site";
|
|
267
|
+
|
|
268
|
+
it("mints the portal once through the hub's route with the cookie init, then redirects there", async () => {
|
|
269
|
+
mockPortalSession.mockResolvedValue({ ok: true, body: { url: PORTAL_URL } });
|
|
270
|
+
await openBillingPortal();
|
|
271
|
+
expect(mockPortalSession).toHaveBeenCalledTimes(1);
|
|
272
|
+
expect(mockPortalSession).toHaveBeenCalledWith({ headers: { cookie: "session=abc" } });
|
|
273
|
+
expect(mockRedirect).toHaveBeenCalledTimes(1);
|
|
274
|
+
expect(mockRedirect).toHaveBeenCalledWith(PORTAL_URL);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it.each([
|
|
278
|
+
["a non-2xx", () => mockPortalSession.mockResolvedValue({ ok: false, status: 404, body: {} }), "[openBillingPortal] status=404"],
|
|
279
|
+
["a throw", () => mockPortalSession.mockRejectedValue(new TypeError("fetch failed")), "[openBillingPortal] errName=TypeError"],
|
|
280
|
+
])("%s redirects nowhere and logs only a status or a name", async (_label, arrange, line) => {
|
|
281
|
+
const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
|
|
282
|
+
arrange();
|
|
283
|
+
await openBillingPortal();
|
|
284
|
+
expect(mockRedirect).not.toHaveBeenCalled();
|
|
285
|
+
expect(errorSpy.mock.calls.map((args) => args.join(" "))).toEqual([line]);
|
|
286
|
+
errorSpy.mockRestore();
|
|
287
|
+
});
|
|
288
|
+
});
|