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
|
@@ -194,8 +194,12 @@ function buildSocialProviders(): SocialProviders {
|
|
|
194
194
|
* "up" — and `apps/web/.env.example` has always documented the variable as
|
|
195
195
|
* REQUIRED in production. The change makes the file honest rather than adding
|
|
196
196
|
* a requirement.
|
|
197
|
+
*
|
|
198
|
+
* Exported (#8146 D5) so the hub's sign-in continuation seals its ref under the
|
|
199
|
+
* same resolved secret instead of re-reading the variable: the second consumer
|
|
200
|
+
* after `betterAuth({ secret })`.
|
|
197
201
|
*/
|
|
198
|
-
function resolveSecret(): string {
|
|
202
|
+
export function resolveSecret(): string {
|
|
199
203
|
const configured = process.env.BETTER_AUTH_SECRET;
|
|
200
204
|
if (configured) return configured;
|
|
201
205
|
|
|
@@ -320,6 +324,24 @@ export function createAuth(deps: CreateAuthDeps = {}) {
|
|
|
320
324
|
const sendResetPassword = makeSendResetPassword({
|
|
321
325
|
prisma,
|
|
322
326
|
send: authEmailTransport,
|
|
327
|
+
/**
|
|
328
|
+
* Who is signed in on the reset request (#8431 D4) — the Security tab's
|
|
329
|
+
* Set password, for a person with no password yet. Read through THIS
|
|
330
|
+
* instance (assigned below; the callback runs only during a request, by
|
|
331
|
+
* which time it exists), never a second one, and never by verifying the
|
|
332
|
+
* cookie by hand. `disableRefresh`: the mail callback runs in the
|
|
333
|
+
* background after the response, where a refresh's cookie write would
|
|
334
|
+
* throw and drop the mail. No request (a server-side call carries no
|
|
335
|
+
* cookie) or no `api` (a test double of `betterAuth`) is no session.
|
|
336
|
+
*/
|
|
337
|
+
sessionUserId: async (request) => {
|
|
338
|
+
if (request === undefined || Reflect.get(instance, "api") === undefined) return null;
|
|
339
|
+
const session = await instance.api.getSession({
|
|
340
|
+
headers: request.headers,
|
|
341
|
+
query: { disableRefresh: true },
|
|
342
|
+
});
|
|
343
|
+
return session?.user.id ?? null;
|
|
344
|
+
},
|
|
323
345
|
});
|
|
324
346
|
const sendVerificationEmail = makeSendVerificationEmail({
|
|
325
347
|
send: authEmailTransport,
|
|
@@ -415,7 +437,7 @@ export function createAuth(deps: CreateAuthDeps = {}) {
|
|
|
415
437
|
}
|
|
416
438
|
};
|
|
417
439
|
|
|
418
|
-
|
|
440
|
+
const instance = betterAuth({
|
|
419
441
|
database: prismaAdapter(prisma, {
|
|
420
442
|
provider: "postgresql",
|
|
421
443
|
}),
|
|
@@ -639,6 +661,7 @@ export function createAuth(deps: CreateAuthDeps = {}) {
|
|
|
639
661
|
]
|
|
640
662
|
: [nextCookies()],
|
|
641
663
|
});
|
|
664
|
+
return instance;
|
|
642
665
|
}
|
|
643
666
|
|
|
644
667
|
// Lazy singleton: resolved on first access, not at module-load time.
|
|
@@ -1,31 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for lib/account-config.ts — the base account area's contributions
|
|
3
|
-
* (#7429, spec 7423 U3 · AC-6; amended by #7448, U5 · D6a/D8a/D9
|
|
3
|
+
* (#7429, spec 7423 U3 · AC-6; amended by #7448, U5 · D6a/D8a/D9; by #8231,
|
|
4
|
+
* spec 8231 D4–D7).
|
|
4
5
|
*
|
|
5
6
|
* Covers:
|
|
6
|
-
* 1. The
|
|
7
|
-
* Settings section (modules add those)
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* 1. The five contribution arrays are exported; the base area ships no
|
|
8
|
+
* Settings section or Billing section (modules add those), and the
|
|
9
|
+
* three New actions (#8233 D2).
|
|
10
|
+
* 2. The Inbox kinds: `result`, `piece` and `message` in `updates` (All
|
|
11
|
+
* only), `purchase` in `purchases`, `appointment` in `upcoming`.
|
|
10
12
|
* 3. `email-unverified` and `payment-due` are For-you sources — needs-you
|
|
11
13
|
* items with an Open action to User settings, never Inbox rows.
|
|
12
|
-
* 4. Each contribution maps its
|
|
13
|
-
* `
|
|
14
|
-
* `
|
|
15
|
-
*
|
|
14
|
+
* 4. Each contribution maps its read: the session for `email-unverified`,
|
|
15
|
+
* `waitlistSignups.me` per configured key for `result`,
|
|
16
|
+
* `member.purchases` for `purchase` (never `flowCheckouts.me`),
|
|
17
|
+
* `member.appointments` for `appointment`, and `subscription.me`'s own
|
|
18
|
+
* `actionRequired` for `payment-due`.
|
|
16
19
|
* 5. A 404 — or a 401/403 while the email is unverified — is "nothing for
|
|
17
20
|
* this caller" (`[]`); any other failed read rejects, a 401/403 for a
|
|
18
21
|
* verified email included, so the Inbox can show an error row (D12).
|
|
22
|
+
* 6. The member kinds call on a cookie-session site — no hub-credential
|
|
23
|
+
* gate (#8231 D7).
|
|
24
|
+
* 7. #8233 D1, D2 — each New action is offered while its member read
|
|
25
|
+
* answers: a 404 (or a switched-off request form, or an unverified
|
|
26
|
+
* 401/403) is `false`, any other failure rejects. The popups are bound
|
|
27
|
+
* in the route (`account/new-popups.tsx`, tested beside it): this file
|
|
28
|
+
* imports nothing under `(authed)`.
|
|
19
29
|
*/
|
|
20
30
|
|
|
21
31
|
const mockSignupMe = jest.fn();
|
|
22
32
|
const mockCheckoutsMe = jest.fn();
|
|
23
33
|
const mockSubscriptionMe = jest.fn();
|
|
34
|
+
const mockMemberPieces = jest.fn();
|
|
35
|
+
const mockMemberMessages = jest.fn();
|
|
36
|
+
const mockMemberPurchases = jest.fn();
|
|
37
|
+
const mockMemberAppointments = jest.fn();
|
|
38
|
+
const mockMemberMeetingTypes = jest.fn();
|
|
39
|
+
const mockMemberRequestForm = jest.fn();
|
|
24
40
|
jest.mock("@working-theory/api-client", () => ({
|
|
25
41
|
__esModule: true,
|
|
26
42
|
waitlistSignups: { me: (...args: unknown[]) => mockSignupMe(...args) },
|
|
27
43
|
flowCheckouts: { me: (...args: unknown[]) => mockCheckoutsMe(...args) },
|
|
28
44
|
subscription: { me: (...args: unknown[]) => mockSubscriptionMe(...args) },
|
|
45
|
+
member: {
|
|
46
|
+
pieces: (...args: unknown[]) => mockMemberPieces(...args),
|
|
47
|
+
messages: (...args: unknown[]) => mockMemberMessages(...args),
|
|
48
|
+
purchases: (...args: unknown[]) => mockMemberPurchases(...args),
|
|
49
|
+
appointments: (...args: unknown[]) => mockMemberAppointments(...args),
|
|
50
|
+
meetingTypes: (...args: unknown[]) => mockMemberMeetingTypes(...args),
|
|
51
|
+
requestForm: (...args: unknown[]) => mockMemberRequestForm(...args),
|
|
52
|
+
},
|
|
29
53
|
}));
|
|
30
54
|
|
|
31
55
|
import type {
|
|
@@ -33,12 +57,16 @@ import type {
|
|
|
33
57
|
AccountForYouSource,
|
|
34
58
|
AccountInboxKind,
|
|
35
59
|
} from "@working-theory/templates/account";
|
|
60
|
+
import { requestQuoteFlowConfig } from "@working-theory/validation";
|
|
36
61
|
|
|
37
62
|
import {
|
|
63
|
+
accountBillingSections,
|
|
38
64
|
accountExperimentKeys,
|
|
39
65
|
accountForYouSources,
|
|
40
66
|
accountInboxKinds,
|
|
67
|
+
accountNewActions,
|
|
41
68
|
accountSettingsSections,
|
|
69
|
+
type AccountNewRead,
|
|
42
70
|
} from "../account-config";
|
|
43
71
|
|
|
44
72
|
const INIT = { headers: { cookie: "session=abc" } };
|
|
@@ -79,17 +107,39 @@ const SIGNUP = {
|
|
|
79
107
|
answers: { q3: null },
|
|
80
108
|
};
|
|
81
109
|
|
|
82
|
-
const
|
|
83
|
-
id: "
|
|
84
|
-
|
|
85
|
-
experimentKey: "diag",
|
|
86
|
-
offerKey: "pro",
|
|
87
|
-
status: "completed",
|
|
110
|
+
const PURCHASE = {
|
|
111
|
+
id: "pur_1",
|
|
112
|
+
offering: "Pro plan",
|
|
88
113
|
amountMinor: 4900,
|
|
89
114
|
currency: "USD",
|
|
90
115
|
period: "month",
|
|
91
|
-
|
|
92
|
-
|
|
116
|
+
purchasedAt: "2026-09-02T10:00:00.000Z",
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const APPOINTMENT = {
|
|
120
|
+
id: "appt_1",
|
|
121
|
+
title: "Kickoff",
|
|
122
|
+
startTime: "2026-10-20T15:00:00.000Z",
|
|
123
|
+
endTime: "2026-10-20T15:30:00.000Z",
|
|
124
|
+
status: "confirmed" as const,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const PIECE = {
|
|
128
|
+
id: "piece_1",
|
|
129
|
+
title: "Brand guide",
|
|
130
|
+
stage: "in_progress",
|
|
131
|
+
needsClient: true,
|
|
132
|
+
createdAt: "2026-09-01T10:00:00.000Z",
|
|
133
|
+
updatedAt: "2026-09-03T10:00:00.000Z",
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const MESSAGE = {
|
|
137
|
+
id: "intx_1",
|
|
138
|
+
subject: "Your draft",
|
|
139
|
+
body: "Attached.",
|
|
140
|
+
createdAt: "2026-09-04T10:00:00.000Z",
|
|
141
|
+
file: null,
|
|
142
|
+
readAt: null,
|
|
93
143
|
};
|
|
94
144
|
|
|
95
145
|
const ok = <T>(body: T) => ({ ok: true as const, body });
|
|
@@ -101,22 +151,30 @@ afterEach(() => {
|
|
|
101
151
|
});
|
|
102
152
|
|
|
103
153
|
describe("account-config exports (AC-6)", () => {
|
|
104
|
-
it("exports the
|
|
154
|
+
it("exports the five contribution arrays", () => {
|
|
105
155
|
expect(Array.isArray(accountInboxKinds)).toBe(true);
|
|
106
156
|
expect(accountSettingsSections).toEqual([]);
|
|
107
157
|
expect(Array.isArray(accountForYouSources)).toBe(true);
|
|
108
158
|
});
|
|
109
159
|
|
|
110
|
-
it("ships
|
|
160
|
+
it("ships the three New actions, by message key, and no Billing section (#8233 D2)", () => {
|
|
161
|
+
expect(accountNewActions.map((a) => [a.id, a.label])).toEqual([
|
|
162
|
+
["schedule-service", "new.actions.scheduleService"],
|
|
163
|
+
["request-quote", "new.actions.requestQuote"],
|
|
164
|
+
["send-message", "new.actions.sendMessage"],
|
|
165
|
+
]);
|
|
166
|
+
expect(accountBillingSections).toEqual([]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("ships the base kinds and the member kinds in their sections (#8231 D4–D6)", () => {
|
|
111
170
|
expect(accountInboxKinds.map((k) => [k.kind, k.section])).toEqual([
|
|
112
171
|
["result", "updates"],
|
|
113
|
-
["purchase", "
|
|
172
|
+
["purchase", "purchases"],
|
|
114
173
|
["piece", "updates"],
|
|
115
174
|
["message", "updates"],
|
|
175
|
+
["appointment", "upcoming"],
|
|
116
176
|
]);
|
|
117
|
-
//
|
|
118
|
-
// Inbox row asks for a decision — that is the For-you panel's job.
|
|
119
|
-
expect(accountInboxKinds.some((k) => k.section === "upcoming")).toBe(false);
|
|
177
|
+
// No Inbox row asks for a decision — that is the For-you panel's job.
|
|
120
178
|
expect(accountInboxKinds.some((k) => k.section === "needs-you")).toBe(false);
|
|
121
179
|
});
|
|
122
180
|
|
|
@@ -152,7 +210,7 @@ describe("email-unverified — a For-you item from the session", () => {
|
|
|
152
210
|
id: "email-unverified",
|
|
153
211
|
type: { id: "email", label: "t:profile.email", tone: "needs-you" },
|
|
154
212
|
title: "t:profile.unverified",
|
|
155
|
-
action: { kind: "open", href: "/account/
|
|
213
|
+
action: { kind: "open", href: "/account/general" },
|
|
156
214
|
dismissible: true,
|
|
157
215
|
});
|
|
158
216
|
});
|
|
@@ -191,25 +249,30 @@ describe("result — waitlistSignups.me per accountExperimentKeys", () => {
|
|
|
191
249
|
});
|
|
192
250
|
});
|
|
193
251
|
|
|
194
|
-
describe("purchase —
|
|
195
|
-
|
|
196
|
-
|
|
252
|
+
describe("purchase — member.purchases (#8231 D5)", () => {
|
|
253
|
+
const ITEM = { id: "pur_1", title: "Pro plan", at: PURCHASE.purchasedAt, data: PURCHASE };
|
|
254
|
+
|
|
255
|
+
it("sits in the purchases section", () => {
|
|
256
|
+
expect(kind("purchase").section).toBe("purchases");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("is one item per purchase, at its purchase time, and never reads flowCheckouts.me", async () => {
|
|
260
|
+
mockMemberPurchases.mockResolvedValue(ok({ items: [PURCHASE] }));
|
|
197
261
|
const items = await kind("purchase").load(ctx());
|
|
198
|
-
expect(
|
|
199
|
-
expect(
|
|
200
|
-
|
|
201
|
-
]);
|
|
262
|
+
expect(mockMemberPurchases).toHaveBeenCalledWith(INIT);
|
|
263
|
+
expect(mockCheckoutsMe).not.toHaveBeenCalled();
|
|
264
|
+
expect(items).toEqual([ITEM]);
|
|
202
265
|
});
|
|
203
266
|
|
|
204
267
|
it("is empty on a 404", async () => {
|
|
205
|
-
|
|
268
|
+
mockMemberPurchases.mockResolvedValue(fail(404));
|
|
206
269
|
await expect(kind("purchase").load(ctx())).resolves.toEqual([]);
|
|
207
270
|
});
|
|
208
271
|
|
|
209
272
|
it.each([401, 403])(
|
|
210
273
|
"is empty on a %i while the email is unverified",
|
|
211
274
|
async (status) => {
|
|
212
|
-
|
|
275
|
+
mockMemberPurchases.mockResolvedValue(fail(status));
|
|
213
276
|
await expect(
|
|
214
277
|
kind("purchase").load(ctx({ emailVerified: false })),
|
|
215
278
|
).resolves.toEqual([]);
|
|
@@ -219,33 +282,114 @@ describe("purchase — flowCheckouts.me", () => {
|
|
|
219
282
|
it.each([401, 403])(
|
|
220
283
|
"rejects on a %i for a verified email",
|
|
221
284
|
async (status) => {
|
|
222
|
-
|
|
285
|
+
mockMemberPurchases.mockResolvedValue(fail(status));
|
|
223
286
|
await expect(kind("purchase").load(ctx())).rejects.toMatchObject({
|
|
224
287
|
status,
|
|
225
288
|
});
|
|
226
289
|
},
|
|
227
290
|
);
|
|
228
291
|
|
|
229
|
-
it("rejects on a
|
|
230
|
-
|
|
292
|
+
it("rejects on a 503", async () => {
|
|
293
|
+
mockMemberPurchases.mockResolvedValue(fail(503));
|
|
231
294
|
await expect(kind("purchase").load(ctx())).rejects.toThrow();
|
|
232
295
|
});
|
|
233
296
|
|
|
234
|
-
it("details the
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
title: "pro",
|
|
238
|
-
at: CHECKOUT.completedAt,
|
|
239
|
-
data: CHECKOUT,
|
|
240
|
-
};
|
|
241
|
-
const detail = kind("purchase").renderDetail(item, ctx());
|
|
242
|
-
expect(detail.status).toBe("completed");
|
|
297
|
+
it("details the purchase's fields, with no status", () => {
|
|
298
|
+
const detail = kind("purchase").renderDetail(ITEM, ctx());
|
|
299
|
+
expect(detail.status).toBeUndefined();
|
|
243
300
|
expect(detail.fields?.items.map((i) => i.label)).toEqual([
|
|
244
301
|
"t:purchases.offer",
|
|
245
302
|
"t:purchases.amount",
|
|
246
303
|
"t:purchases.period",
|
|
247
304
|
"t:purchases.date",
|
|
248
305
|
]);
|
|
306
|
+
expect(detail.fields?.items[0]?.value).toBe("Pro plan");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("shows Not recorded for a null amount or period", () => {
|
|
310
|
+
const data = { ...PURCHASE, amountMinor: null, currency: null, period: null };
|
|
311
|
+
const detail = kind("purchase").renderDetail({ ...ITEM, data }, ctx());
|
|
312
|
+
const values = detail.fields?.items.map((i) => i.value);
|
|
313
|
+
expect(values?.[1]).toBe("t:purchases.unavailable");
|
|
314
|
+
expect(values?.[2]).toBe("t:purchases.unavailable");
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe("appointment — member.appointments (#8231 D6)", () => {
|
|
319
|
+
const PAST = {
|
|
320
|
+
...APPOINTMENT,
|
|
321
|
+
id: "appt_0",
|
|
322
|
+
title: null,
|
|
323
|
+
startTime: "2026-09-10T15:00:00.000Z",
|
|
324
|
+
endTime: "2026-09-10T15:30:00.000Z",
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
it("sits in the upcoming section, named by its message key", () => {
|
|
328
|
+
expect(kind("appointment").section).toBe("upcoming");
|
|
329
|
+
expect(kind("appointment").label).toBe("appointments.heading");
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("returns the upcoming items then the past, flagged, at their start time", async () => {
|
|
333
|
+
mockMemberAppointments.mockResolvedValue(ok({ upcoming: [APPOINTMENT], past: [PAST] }));
|
|
334
|
+
const items = await kind("appointment").load(ctx());
|
|
335
|
+
expect(mockMemberAppointments).toHaveBeenCalledWith(INIT);
|
|
336
|
+
expect(items).toEqual([
|
|
337
|
+
{ id: "appt_1", title: "Kickoff", at: APPOINTMENT.startTime, upcoming: true, data: APPOINTMENT },
|
|
338
|
+
{ id: "appt_0", title: "t:appointments.untitled", at: PAST.startTime, upcoming: false, data: PAST },
|
|
339
|
+
]);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("is empty on a 404", async () => {
|
|
343
|
+
mockMemberAppointments.mockResolvedValue(fail(404));
|
|
344
|
+
await expect(kind("appointment").load(ctx())).resolves.toEqual([]);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("rejects on a 503", async () => {
|
|
348
|
+
mockMemberAppointments.mockResolvedValue(fail(503));
|
|
349
|
+
await expect(kind("appointment").load(ctx())).rejects.toThrow();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("details the status and the start and end", () => {
|
|
353
|
+
const item = { id: "appt_1", title: "Kickoff", at: APPOINTMENT.startTime, upcoming: true, data: APPOINTMENT };
|
|
354
|
+
const detail = kind("appointment").renderDetail(item, ctx());
|
|
355
|
+
expect(detail.status).toBe("t:appointments.statuses.confirmed");
|
|
356
|
+
expect(detail.fields?.items.map((i) => i.label)).toEqual([
|
|
357
|
+
"t:appointments.starts",
|
|
358
|
+
"t:appointments.ends",
|
|
359
|
+
]);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe("member kinds on a cookie-session site — no gate (#8231 D7)", () => {
|
|
364
|
+
it("piece reads member.pieces with the cookie init and returns its rows", async () => {
|
|
365
|
+
mockMemberPieces.mockResolvedValue(ok({ items: [PIECE] }));
|
|
366
|
+
const items = await kind("piece").load(ctx());
|
|
367
|
+
expect(mockMemberPieces).toHaveBeenCalledWith(INIT);
|
|
368
|
+
expect(items.map((i) => i.id)).toEqual(["piece_1"]);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("message reads member.messages with the cookie init and returns its rows", async () => {
|
|
372
|
+
mockMemberMessages.mockResolvedValue(ok({ items: [MESSAGE] }));
|
|
373
|
+
const items = await kind("message").load(ctx());
|
|
374
|
+
expect(mockMemberMessages).toHaveBeenCalledWith(INIT);
|
|
375
|
+
expect(items.map((i) => i.id)).toEqual(["intx_1"]);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("piece-needs-client reads member.pieces with the cookie init", async () => {
|
|
379
|
+
mockMemberPieces.mockResolvedValue(ok({ items: [PIECE] }));
|
|
380
|
+
const items = await source("piece-needs-client").load(ctx());
|
|
381
|
+
expect(mockMemberPieces).toHaveBeenCalledWith(INIT);
|
|
382
|
+
expect(items.map((i) => i.id)).toEqual(["piece:piece_1"]);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it.each([
|
|
386
|
+
["piece", mockMemberPieces],
|
|
387
|
+
["message", mockMemberMessages],
|
|
388
|
+
] as const)("%s loads [] on a 404 and rejects on a 500", async (id, mock) => {
|
|
389
|
+
mock.mockResolvedValue(fail(404));
|
|
390
|
+
await expect(kind(id).load(ctx())).resolves.toEqual([]);
|
|
391
|
+
mock.mockResolvedValue(fail(500));
|
|
392
|
+
await expect(kind(id).load(ctx())).rejects.toThrow();
|
|
249
393
|
});
|
|
250
394
|
});
|
|
251
395
|
|
|
@@ -266,7 +410,7 @@ describe("payment-due — a For-you item from subscription.me's actionRequired",
|
|
|
266
410
|
id: "payment-due",
|
|
267
411
|
type: { id: "payment", label: "t:subscription.action", tone: "needs-you" },
|
|
268
412
|
title: "t:subscription.headline",
|
|
269
|
-
action: { kind: "open", href: "/account/
|
|
413
|
+
action: { kind: "open", href: "/account/billing" },
|
|
270
414
|
dismissible: true,
|
|
271
415
|
});
|
|
272
416
|
});
|
|
@@ -286,3 +430,71 @@ describe("payment-due — a For-you item from subscription.me's actionRequired",
|
|
|
286
430
|
await expect(source("payment-due").load(ctx())).rejects.toThrow();
|
|
287
431
|
});
|
|
288
432
|
});
|
|
433
|
+
|
|
434
|
+
// ── #8233 — the New actions ──────────────────────────────────────────────────
|
|
435
|
+
|
|
436
|
+
describe("New actions — offered while their member read answers (#8233 D1, D2)", () => {
|
|
437
|
+
function action(id: string): AccountNewRead {
|
|
438
|
+
const found = accountNewActions.find((a) => a.id === id);
|
|
439
|
+
if (!found) throw new Error(`no New action ${id}`);
|
|
440
|
+
return found;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const TYPE = { slug: "consult", title: "Consultation", durationMinutes: 30 };
|
|
444
|
+
const FORM = {
|
|
445
|
+
form: {
|
|
446
|
+
title: "Request a quote",
|
|
447
|
+
defaultLocale: "en",
|
|
448
|
+
steps: requestQuoteFlowConfig.steps,
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
const failure = (status: number) => ({
|
|
452
|
+
ok: false as const,
|
|
453
|
+
status,
|
|
454
|
+
body: { ok: false, error: { code: "failed", message: "x" } },
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it.each([
|
|
458
|
+
["{ items: [] }", true, ok({ items: [] })],
|
|
459
|
+
["one type", true, ok({ items: [TYPE] })],
|
|
460
|
+
["404", false, fail(404)],
|
|
461
|
+
])("schedule-service: meetingTypes %s → %s", async (_name, offered, answer) => {
|
|
462
|
+
mockMemberMeetingTypes.mockResolvedValue(answer);
|
|
463
|
+
await expect(action("schedule-service").available(ctx())).resolves.toBe(offered);
|
|
464
|
+
expect(mockMemberMeetingTypes).toHaveBeenCalledWith(INIT);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it.each([
|
|
468
|
+
["a form", true, ok(FORM)],
|
|
469
|
+
["{ form: null }", false, ok({ form: null })],
|
|
470
|
+
["404", false, fail(404)],
|
|
471
|
+
])("request-quote: requestForm %s → %s", async (_name, offered, answer) => {
|
|
472
|
+
mockMemberRequestForm.mockResolvedValue(answer);
|
|
473
|
+
await expect(action("request-quote").available(ctx())).resolves.toBe(offered);
|
|
474
|
+
expect(mockMemberRequestForm).toHaveBeenCalledWith(INIT);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("send-message: a messages 200 → true, read with ctx.init", async () => {
|
|
478
|
+
mockMemberMessages.mockResolvedValue(ok({ items: [] }));
|
|
479
|
+
await expect(action("send-message").available(ctx())).resolves.toBe(true);
|
|
480
|
+
expect(mockMemberMessages).toHaveBeenCalledWith(INIT);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("send-message: 404 → false; 403 while the email is unverified → false", async () => {
|
|
484
|
+
mockMemberMessages.mockResolvedValue(fail(404));
|
|
485
|
+
await expect(action("send-message").available(ctx())).resolves.toBe(false);
|
|
486
|
+
mockMemberMessages.mockResolvedValue(fail(403));
|
|
487
|
+
await expect(action("send-message").available(ctx({ emailVerified: false }))).resolves.toBe(
|
|
488
|
+
false,
|
|
489
|
+
);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it.each([
|
|
493
|
+
["schedule-service", 503, mockMemberMeetingTypes],
|
|
494
|
+
["request-quote", 500, mockMemberRequestForm],
|
|
495
|
+
["send-message", 503, mockMemberMessages],
|
|
496
|
+
] as const)("%s rejects on a %i — a failed read is never reported as absent", async (id, status, mock) => {
|
|
497
|
+
mock.mockResolvedValue(failure(status));
|
|
498
|
+
await expect(action(id).available(ctx())).rejects.toMatchObject({ status });
|
|
499
|
+
});
|
|
500
|
+
});
|
|
@@ -152,6 +152,90 @@ describe("sendResetPassword (§ D14 — the credential-account check)", () => {
|
|
|
152
152
|
});
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
+
describe("sendResetPassword — the signed-in carve-out (#8431 D4)", () => {
|
|
156
|
+
const USER = { id: "user_1", email: "person@example.com" };
|
|
157
|
+
const RESET_URL = "https://site.example/api/auth/reset-password/tok";
|
|
158
|
+
const REQUEST = { headers: new Headers({ cookie: "session=abc" }) } as Request;
|
|
159
|
+
|
|
160
|
+
it("sends ONE password-reset to the account's own address when the session is that person's", async () => {
|
|
161
|
+
const send = jest.fn().mockResolvedValue({ ok: true, body: { ok: true } });
|
|
162
|
+
const { lines, log } = makeLog();
|
|
163
|
+
const sessionUserId = jest.fn().mockResolvedValue("user_1");
|
|
164
|
+
const cb = makeSendResetPassword({ prisma: makePrisma(false), send, log, sessionUserId });
|
|
165
|
+
|
|
166
|
+
await cb({ user: USER, url: RESET_URL }, REQUEST);
|
|
167
|
+
|
|
168
|
+
expect(sessionUserId).toHaveBeenCalledWith(REQUEST);
|
|
169
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
170
|
+
const [rawBody] = send.mock.calls[0] as [string];
|
|
171
|
+
expect(JSON.parse(rawBody)).toEqual({
|
|
172
|
+
kind: "password-reset",
|
|
173
|
+
userId: "user_1",
|
|
174
|
+
to: "person@example.com",
|
|
175
|
+
url: RESET_URL,
|
|
176
|
+
expiresInMinutes: 60,
|
|
177
|
+
});
|
|
178
|
+
expect(lines.some((l) => l.fields.reason === "self_service_set")).toBe(true);
|
|
179
|
+
expect(JSON.stringify(lines)).not.toContain("person@example.com");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it.each<[string, () => Promise<string | null>]>([
|
|
183
|
+
["another person's id", () => Promise.resolve("user_2")],
|
|
184
|
+
["null (no session)", () => Promise.resolve(null)],
|
|
185
|
+
["a rejection", () => Promise.reject(new Error("db down"))],
|
|
186
|
+
])("sends NOTHING and logs no address when the session lookup answers %s", async (_label, impl) => {
|
|
187
|
+
const send = jest.fn();
|
|
188
|
+
const { lines, log } = makeLog();
|
|
189
|
+
const cb = makeSendResetPassword({ prisma: makePrisma(false), send, log, sessionUserId: jest.fn(impl) });
|
|
190
|
+
|
|
191
|
+
await expect(cb({ user: USER, url: RESET_URL }, REQUEST)).resolves.toBeUndefined();
|
|
192
|
+
|
|
193
|
+
expect(send).not.toHaveBeenCalled();
|
|
194
|
+
expect(lines.some((l) => l.fields.reason === "no_credential_account")).toBe(true);
|
|
195
|
+
expect(JSON.stringify(lines)).not.toContain("person@example.com");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("logs the failed lookup's kind only", async () => {
|
|
199
|
+
const { lines, log } = makeLog();
|
|
200
|
+
const cb = makeSendResetPassword({
|
|
201
|
+
prisma: makePrisma(false),
|
|
202
|
+
send: jest.fn(),
|
|
203
|
+
log,
|
|
204
|
+
sessionUserId: jest.fn().mockRejectedValue(new Error("person@example.com broke")),
|
|
205
|
+
});
|
|
206
|
+
await cb({ user: USER, url: RESET_URL }, REQUEST);
|
|
207
|
+
expect(lines).toEqual([
|
|
208
|
+
{
|
|
209
|
+
level: "warn",
|
|
210
|
+
fields: {
|
|
211
|
+
userId: "user_1",
|
|
212
|
+
kind: "password-reset",
|
|
213
|
+
reason: "no_credential_account",
|
|
214
|
+
errorKind: "session_lookup_failed",
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("with no sessionUserId it sends nothing, as before", async () => {
|
|
221
|
+
const send = jest.fn();
|
|
222
|
+
const { log } = makeLog();
|
|
223
|
+
const cb = makeSendResetPassword({ prisma: makePrisma(false), send, log });
|
|
224
|
+
await cb({ user: USER, url: RESET_URL }, REQUEST);
|
|
225
|
+
expect(send).not.toHaveBeenCalled();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("a person WITH a password never needs the session lookup", async () => {
|
|
229
|
+
const send = jest.fn().mockResolvedValue({ ok: true, body: { ok: true } });
|
|
230
|
+
const { log } = makeLog();
|
|
231
|
+
const sessionUserId = jest.fn();
|
|
232
|
+
const cb = makeSendResetPassword({ prisma: makePrisma(true), send, log, sessionUserId });
|
|
233
|
+
await cb({ user: USER, url: RESET_URL });
|
|
234
|
+
expect(sessionUserId).not.toHaveBeenCalled();
|
|
235
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
155
239
|
describe("the § D12 PII boundary", () => {
|
|
156
240
|
it("never logs the address, the token or the assembled link", async () => {
|
|
157
241
|
const send = jest.fn().mockResolvedValue({ ok: true, body: { ok: true } });
|