From a3a49ecdfaf63dabac01d4e7614c53a71e6b89c0 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Thu, 22 Oct 2026 16:47:19 +0300 Subject: [PATCH] feat(membership): password reset request names the template + language the hub will use, or fallback: (mjml-render-failed (401)), and the MJML renderer state Gateway N46 reopened: the admin test reset route answers templateId, templateName, templateSource, language, languageRule, templateLanguages, fallback and mjmlRenderer; --json returns them as they are. The hub's sent entry names the chosen template even when it fell back, so the report now reads fallbackReason first. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- .../membership/password/reset/request.ts | 36 ++++++++- test/membership-password-reset.test.ts | 79 +++++++++++++++++++ 3 files changed, 112 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b171db8..b5145e0 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ norbix scheduler list | `norbix files list/info/get-by-id/upload/download/sign/delete/publish/unpublish` | Upload, download, manage and publish files | | `norbix files integrations test ` | Check that a files integration really works (live upload/read/list/delete probe) | | `norbix users list/get/invite/block/unblock/delete` | Manage project users (membership) | -| `norbix membership password reset request ` | Admin: send a test password-reset e-mail in `--env`; says whether a login was found, which integration sends it, and (from the hub's project-log entry) whether it went out, through which integration and template | +| `norbix membership password reset request ` | Admin: send a test password-reset e-mail in `--env`; says whether a login was found, which integration sends it, which template and language the hub will use (or `fallback: `, e.g. `mjml-render-failed (401)`), the MJML renderer's state, and (from the hub's project-log entry) whether it went out, through which integration and template | | `norbix env list/use/delete/promote/rollback`, `norbix env built-ins repair` | Manage project environments and move content TEST → PROD (see *Environments* below) | | `norbix logs list/trail/get` | Read project logs, follow one correlation ID, show one entry | | `norbix logs write` | Write your app's own lines (`--title …` or `--file entries.json`); prints `accepted: n`, on a 429 says how many seconds to wait | diff --git a/src/commands/membership/password/reset/request.ts b/src/commands/membership/password/reset/request.ts index 0606229..2e72d81 100644 --- a/src/commands/membership/password/reset/request.ts +++ b/src/commands/membership/password/reset/request.ts @@ -13,6 +13,17 @@ export interface TestPasswordResetAnswer { integrationName?: string provider?: string integrationProblem?: string + /** The template and language the hub will use (gateway N46). */ + templateId?: string + templateName?: string + templateSource?: string + language?: string + languageRule?: string + templateLanguages?: string[] + /** Set when the fixed English text would be sent: a code, then the reason (e.g. `mjml-render-failed (401): …`). */ + fallback?: string + /** The MJML renderer: ok, unauthorized (401), unreachable, error (…), not-configured. */ + mjmlRenderer?: string responseStatus?: {isSuccess?: boolean; errors?: Array<{message?: string; errorCode?: string}>} } @@ -32,7 +43,9 @@ export interface DeliveryResult { integrationName?: string provider?: string template?: string + templateName?: string language?: string + languageRule?: string fallbackReason?: string reason?: string } @@ -52,7 +65,9 @@ export function deliveryFrom(entries: LogEntry[]): DeliveryResult { integrationName: m.integrationName, provider: m.provider, template: m.template, + templateName: m.templateName, language: m.language, + languageRule: m.languageRule, fallbackReason: m.fallbackReason, } : {status: 'not-sent', reason: m.reason, integrationId: m.integrationId, fallbackReason: m.fallbackReason} @@ -65,7 +80,10 @@ Admin only (needs the right to change project users' logins). It runs the same reset flow an app runs — the login is looked up in the --env environment, a one-time link is issued and the hub sends the e-mail — and answers what the public request never tells: whether a login was found, the environment, the -integration that will send it, and the correlation id. It then waits for the +integration that will send it, the template and language it will use (or +"fallback: " when the fixed English text would be sent, e.g. +mjml-render-failed (401) when the MJML renderer refuses the hub's token), the +MJML renderer's state, and the correlation id. It then waits for the hub's project-log entry with that correlation id and prints what really happened (sent through which integration and template, or why not). @@ -166,12 +184,22 @@ export function describe(answer: TestPasswordResetAnswer, delivery: DeliveryResu if (answer.integrationProblem) lines.push(` The hub will NOT send it: ${answer.integrationProblem}.`) else if (answer.integrationId) lines.push(` Integration for ${env}: ${answer.integrationName} (${answer.provider}, ${answer.integrationId}).`) + if (answer.queued) { + if (answer.templateId) + lines.push(` Template: ${answer.templateName} (${answer.templateId}, ${answer.templateSource}), language ${answer.language} (${answer.languageRule}; the template has: ${(answer.templateLanguages ?? []).join(', ')}).`) + if (answer.fallback) lines.push(` fallback: ${answer.fallback}`) + if (answer.mjmlRenderer) lines.push(` MJML renderer: ${answer.mjmlRenderer}.`) + } + if (answer.queued) { if (delivery.status === 'sent') { lines.push(`Sent by the hub through ${delivery.integrationName} (${delivery.provider}, ${delivery.integrationId}).`) - lines.push(delivery.template - ? ` Template ${delivery.template}, language ${delivery.language}.` - : ` Fixed English text (template not used: ${delivery.fallbackReason ?? 'unknown'}).`) + // The hub names the chosen template even when it could not render it, so the fallback goes first. + lines.push(delivery.fallbackReason + ? ` Fixed English text — fallback: ${delivery.fallbackReason}` + : delivery.template + ? ` Template ${delivery.templateName ? `${delivery.templateName} (${delivery.template})` : delivery.template}, language ${delivery.language}${delivery.languageRule ? ` (${delivery.languageRule})` : ''}.` + : ' Fixed English text (no template named by the hub).') } else if (delivery.status === 'not-sent') { lines.push(`The hub did NOT send it: ${delivery.reason ?? 'unknown reason'}.`) } else if (waited > 0) { diff --git a/test/membership-password-reset.test.ts b/test/membership-password-reset.test.ts index 063d047..836a243 100644 --- a/test/membership-password-reset.test.ts +++ b/test/membership-password-reset.test.ts @@ -105,7 +105,9 @@ describe('norbix membership password reset request', () => { integrationName: 'Test Me', provider: 'Fake', template: 'tmpl_3ckGBebabo8Z5BiaOgfdG4', + templateName: undefined, language: 'lt', + languageRule: undefined, fallbackReason: undefined, }) }) @@ -158,3 +160,80 @@ describe('the text report', () => { expect(deliveryFrom([{eventCode: 'membership:email:not-sent', meta: {stage: 'send', reason: 'expired before it could be sent'}}]).status).toBe('not-sent') }) }) + +/** + * Gateway N46 (reopened): the admin answer names the template and language + * the hub will use, or `fallback: : ` — Happy Tummy's + * MJML renderer refused the hub's token, so every reset went out as the fixed + * English text while the template and language were fine. + */ +describe('template, language and fallback (N46)', () => { + const chosen = { + ...queued, + templateId: 'tmpl_4OFYLs4G9VYV223hjuAGhR', + templateName: 'Password reset', + templateSource: 'project', + language: 'en', + languageRule: 'english', + templateLanguages: ['en'], + } + const refused = { + ...chosen, + fallback: "mjml-render-failed (401): the MJML renderer refused the hub's token", + mjmlRenderer: 'unauthorized (401)', + } + + it('prints the template and language the hub will use', () => { + expect(report({...chosen, mjmlRenderer: 'ok'}, {status: 'unknown'}, 0)).toBe([ + 'Queued in TEST (correlation 0e563daa3ce7d2389f7ecc185e01656d; the link expires 2026-10-22T08:45:32Z).', + ' Integration for TEST: Test Me (Fake, int_57wC6x4g7mHr2uuUO7hMJB).', + ' Template: Password reset (tmpl_4OFYLs4G9VYV223hjuAGhR, project), language en (english; the template has: en).', + ' MJML renderer: ok.', + ].join('\n')) + }) + + it('prints fallback: mjml-render-failed (401) when the renderer refuses the token', () => { + const text = report(refused, {status: 'unknown'}, 0) + expect(text).toContain(" fallback: mjml-render-failed (401): the MJML renderer refused the hub's token") + expect(text).toContain(' MJML renderer: unauthorized (401).') + }) + + it('puts the fallback in --json', async () => { + fakeFetch([['/membership/userauth/password/reset/test', refused]]) + + const {result, error} = await runCommand<{fallback: string; mjmlRenderer: string; templateId: string; language: string}>([ + 'membership', 'password', 'reset', 'request', 'tester@example.com', '--env', 'TEST', '--wait', '0', '--json', ...globalArgs, + ]) + + expect(error).toBeUndefined() + expect(result?.fallback).toBe("mjml-render-failed (401): the MJML renderer refused the hub's token") + expect(result?.mjmlRenderer).toBe('unauthorized (401)') + expect(result?.templateId).toBe('tmpl_4OFYLs4G9VYV223hjuAGhR') + expect(result?.language).toBe('en') + }) + + it('reads the hub fallback first: the hub names the chosen template even when it could not render it', () => { + const delivery = deliveryFrom([ + { + eventCode: 'membership:email:sent', + meta: { + stage: 'send', + integrationId: 'int_2XKN', + integrationName: 'Happy Tummy TEST', + provider: 'AwsSes', + template: 'tmpl_4OFYLs4G9VYV223hjuAGhR', + templateName: 'Password reset', + language: 'en', + languageRule: 'english', + fallbackReason: "mjml-render-failed (401): the MJML renderer refused the hub's token", + }, + }, + ]) + expect(report(chosen, delivery, 5)).toContain( + " Fixed English text — fallback: mjml-render-failed (401): the MJML renderer refused the hub's token", + ) + expect(report(chosen, {...delivery, fallbackReason: undefined}, 5)).toContain( + ' Template Password reset (tmpl_4OFYLs4G9VYV223hjuAGhR), language en (english).', + ) + }) +})