Skip to content

Conversation

@troyciesco
Copy link
Contributor

@troyciesco troyciesco commented Dec 10, 2025

closes https://linear.app/ghost/issue/NY-788

  • Adds the ability to send a test of member welcome emails in a similar manner to email previews on posts
  • Includes brute protection on the endpoint, same as for posts. It's (currently) also in the same bucket as posts (i.e. if you send 10 test welcome emails and then try to send a post email preview you'd get rate limited)
  • Utilizes edit permissions for sendTestEmail - where edit and sendTestEmail can be pretty distinct on posts depending the permissions of the staff user, in the case of member welcome emails the concepts are pretty tightly coupled (and only admins have permission anyway)

@github-actions github-actions bot added the migration [pull request] Includes migration for review label Dec 10, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 10, 2025

Walkthrough

Adds a test-send feature for automated member welcome emails: a new frontend mutation hook useSendTestWelcomeEmail and UI changes in the welcome-email modal to capture, validate, and send a test address with send-state feedback; a backend admin route POST /automated_emails/:id/test guarded by a rate-limiting middleware and admin auth; input validation for the test payload (email, subject, lexical); MemberWelcomeEmailService.sendTestEmail that validates the AutomatedEmail, renders the provided lexical/subject for a synthetic member, and sends a test message; and new E2E tests for the endpoint (contains a duplicated test block).

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Areas requiring extra attention:

  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx: test-email validation, send-state transitions and timeout cleanup, UI wiring.
  • apps/admin-x-framework/src/api/automated-emails.ts: useSendTestWelcomeEmail request path/body shape and integration with existing save flow.
  • ghost/core/core/server/web/api/endpoints/admin/routes.js: route registration and middleware ordering for POST /automated_emails/:id/test.
  • ghost/core/core/server/api/endpoints/utils/validators/input/automated_emails.js: new validation logic and BadRequestError messages for email/subject/lexical.
  • ghost/core/core/server/services/member-welcome-emails/service.js: AutomatedEmail lookup, NotFound handling, rendering inputs, and mail send invocation.
  • ghost/core/test/e2e-api/admin/automated-emails.test.js: duplicated SendTestEmail suite — verify and deduplicate; mailer stubbing and expected assertions.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding functionality to test member welcome emails, which aligns with all modified files and the PR objectives.
Description check ✅ Passed The description is directly related to the changeset, explaining the feature, rate limiting approach, and permission model used in the implementation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch NY-788_send-test-welcome-emails

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@troyciesco troyciesco force-pushed the NY-788_send-test-welcome-emails branch from 4446c0b to a45fed3 Compare December 10, 2025 23:01
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
ghost/core/core/server/services/member-welcome-emails/service.js (1)

119-123: Consider extracting siteSettings construction to reduce duplication.

The siteSettings object construction is duplicated between the send() method (lines 62-66) and this sendTestEmail() method. Consider extracting this into a private helper method.

Apply this refactor to reduce duplication:

+    #buildSiteSettings() {
+        return {
+            title: settingsCache.get('title') || 'Ghost',
+            url: urlUtils.urlFor('home', true),
+            accentColor: settingsCache.get('accent_color') || '#15212A'
+        };
+    }
+
     async send({member, memberStatus}) {
         // ...
-        const siteSettings = {
-            title: settingsCache.get('title') || 'Ghost',
-            url: urlUtils.urlFor('home', true),
-            accentColor: settingsCache.get('accent_color') || '#15212A'
-        };
+        const siteSettings = this.#buildSiteSettings();
         // ...
     }

     async sendTestEmail({email, automatedEmailId}) {
         // ...
-        const siteSettings = {
-            title: settingsCache.get('title') || 'Ghost',
-            url: urlUtils.urlFor('home', true),
-            accentColor: settingsCache.get('accent_color') || '#15212A'
-        };
+        const siteSettings = this.#buildSiteSettings();
         // ...
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 287d2a3 and 4446c0b.

📒 Files selected for processing (6)
  • apps/admin-x-framework/src/api/automated-emails.ts (1 hunks)
  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx (5 hunks)
  • ghost/core/core/server/api/endpoints/automated-emails.js (2 hunks)
  • ghost/core/core/server/data/schema/fixtures/fixtures.json (1 hunks)
  • ghost/core/core/server/services/member-welcome-emails/service.js (1 hunks)
  • ghost/core/core/server/web/api/endpoints/admin/routes.js (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-30T17:13:26.190Z
Learnt from: sam-lord
Repo: TryGhost/Ghost PR: 25303
File: ghost/core/core/server/services/email-service/BatchSendingService.js:19-19
Timestamp: 2025-10-30T17:13:26.190Z
Learning: In ghost/core/core/server/services/email-service/BatchSendingService.js and similar files in the Ghost codebase, prefer using `{...options}` spread syntax without explicit guards like `...(options || {})` when spreading potentially undefined objects, as the maintainer prefers cleaner syntax over defensive patterns when the behavior is safe.

Applied to files:

  • ghost/core/core/server/services/member-welcome-emails/service.js
📚 Learning: 2025-11-25T14:28:50.351Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T14:28:50.351Z
Learning: API routes should be defined in `ghost/core/core/server/api/`

Applied to files:

  • ghost/core/core/server/web/api/endpoints/admin/routes.js
🧬 Code graph analysis (3)
apps/admin-x-framework/src/api/automated-emails.ts (1)
apps/admin-x-framework/src/utils/api/hooks.ts (1)
  • createMutation (184-215)
ghost/core/core/server/api/endpoints/automated-emails.js (4)
ghost/core/core/server/services/member-welcome-emails/service.js (2)
  • require (9-9)
  • require (11-11)
ghost/core/core/server/web/api/endpoints/admin/routes.js (1)
  • require (3-3)
ghost/core/core/server/api/endpoints/utils/validators/input/automated_emails.js (1)
  • require (4-4)
ghost/core/test/e2e-api/admin/automated-emails.test.js (1)
  • require (1-1)
apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx (1)
apps/admin-x-framework/src/api/automated-emails.ts (1)
  • useSendTestWelcomeEmail (52-56)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: ActivityPub tests
  • GitHub Check: Acceptance tests (Node 22.18.0, sqlite3)
  • GitHub Check: Acceptance tests (Node 22.18.0, mysql8)
  • GitHub Check: Admin-X Settings tests
  • GitHub Check: Ghost-CLI tests
  • GitHub Check: Legacy tests (Node 22.18.0, sqlite3)
  • GitHub Check: Legacy tests (Node 22.18.0, mysql8)
  • GitHub Check: Lint
  • GitHub Check: Unit tests (Node 22.18.0)
  • GitHub Check: Build & Push Docker Image
🔇 Additional comments (7)
ghost/core/core/server/data/schema/fixtures/fixtures.json (1)

539-543: LGTM!

The new permission entry for sending test welcome emails is properly structured and follows the established pattern used for other permissions in the system.

ghost/core/core/server/web/api/endpoints/admin/routes.js (1)

186-186: LGTM!

The new test email route is properly secured with brute-force protection and admin authentication, following the same pattern as the email preview test endpoint (line 343).

apps/admin-x-framework/src/api/automated-emails.ts (1)

52-56: LGTM!

The mutation hook is properly typed and follows the established pattern for API mutations in this file.

ghost/core/core/server/api/endpoints/automated-emails.js (3)

4-4: LGTM!

The import is necessary for the new sendTestEmail functionality.


142-148: Service initialization is idempotent and safe.

The memberWelcomeEmailService.init() method uses a guard clause (if (this.api) { return; }) to ensure the service is only initialized once. Subsequent calls on every request are harmless and incur negligible overhead.


139-141: Remove this review comment—the factual basis is incorrect.

The fixtures.json does not contain a sendTestEmail permission for automated_email. The permissions defined for automated_email are: browse, read, edit, add, and destroy (lines 516–537). The sendTestEmail action type only exists for email_preview resource (line 471), not for automated_email.

The endpoint correctly uses method: 'edit' which is a valid permission for the automated_email resource. There is no inconsistency with email preview endpoints—they use different resources (email_preview vs automated_email) with different permission models.

Likely an incorrect or invalid review comment.

apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx (1)

2-2: LGTM!

The imports and state management for the test email functionality are well-structured.

Also applies to: 11-11, 50-50, 53-54

Comment on lines 116 to 135
const lexical = automatedEmail.get('lexical');
const subject = automatedEmail.get('subject');

const siteSettings = {
title: settingsCache.get('title') || 'Ghost',
url: urlUtils.urlFor('home', true),
accentColor: settingsCache.get('accent_color') || '#15212A'
};

const testMember = {
name: 'Jamie Larson',
email: email
};

const {html, text, subject: renderedSubject} = await this.#renderer.render({
lexical,
subject,
member: testMember,
siteSettings
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate lexical content before rendering.

The method doesn't verify that lexical contains valid content before passing it to the renderer. If the AutomatedEmail has a null or empty lexical field, the renderer might fail or produce unexpected output.

Consider adding validation after line 117:

 const lexical = automatedEmail.get('lexical');
 const subject = automatedEmail.get('subject');
+
+if (!lexical) {
+    throw new errors.ValidationError({
+        message: 'Email content is required to send a test email'
+    });
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const lexical = automatedEmail.get('lexical');
const subject = automatedEmail.get('subject');
const siteSettings = {
title: settingsCache.get('title') || 'Ghost',
url: urlUtils.urlFor('home', true),
accentColor: settingsCache.get('accent_color') || '#15212A'
};
const testMember = {
name: 'Jamie Larson',
email: email
};
const {html, text, subject: renderedSubject} = await this.#renderer.render({
lexical,
subject,
member: testMember,
siteSettings
});
const lexical = automatedEmail.get('lexical');
const subject = automatedEmail.get('subject');
if (!lexical) {
throw new errors.ValidationError({
message: 'Email content is required to send a test email'
});
}
const siteSettings = {
title: settingsCache.get('title') || 'Ghost',
url: urlUtils.urlFor('home', true),
accentColor: settingsCache.get('accent_color') || '#15212A'
};
const testMember = {
name: 'Jamie Larson',
email: email
};
const {html, text, subject: renderedSubject} = await this.#renderer.render({
lexical,
subject,
member: testMember,
siteSettings
});
🤖 Prompt for AI Agents
In ghost/core/core/server/services/member-welcome-emails/service.js around lines
116 to 135, the code passes automatedEmail.get('lexical') to the renderer
without validating it; add a check after retrieving lexical (line ~117) to
ensure it is a non-empty string or contains expected content, and if not, either
assign a safe default lexical/template or return/throw a clear error so
rendering is not attempted with null/empty input; include a brief log or error
message indicating the automated email id/subject is missing lexical content to
aid debugging.

@troyciesco troyciesco force-pushed the NY-788_send-test-welcome-emails branch 2 times, most recently from 0677b52 to 56087c9 Compare December 10, 2025 23:04
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
ghost/core/core/server/services/member-welcome-emails/service.js (1)

116-135: Validate lexical content before rendering.

The method retrieves lexical from the automated email but doesn't validate it before passing to the renderer. If lexical is null or empty, the renderer may fail or produce unexpected output. This mirrors the validation done in loadMemberWelcomeEmails (line 28).

 const lexical = automatedEmail.get('lexical');
 const subject = automatedEmail.get('subject');
+
+if (!lexical) {
+    throw new errors.ValidationError({
+        message: MESSAGES.INVALID_LEXICAL_STRUCTURE
+    });
+}
🧹 Nitpick comments (1)
ghost/core/core/server/services/member-welcome-emails/service.js (1)

119-123: Consider extracting siteSettings construction to a helper.

This block duplicates the same logic from lines 62-66 in the send method. A small private helper like #getSiteSettings() would reduce duplication.

+    #getSiteSettings() {
+        return {
+            title: settingsCache.get('title') || 'Ghost',
+            url: urlUtils.urlFor('home', true),
+            accentColor: settingsCache.get('accent_color') || '#15212A'
+        };
+    }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4446c0b and 56087c9.

📒 Files selected for processing (5)
  • apps/admin-x-framework/src/api/automated-emails.ts (1 hunks)
  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx (5 hunks)
  • ghost/core/core/server/api/endpoints/automated-emails.js (2 hunks)
  • ghost/core/core/server/services/member-welcome-emails/service.js (1 hunks)
  • ghost/core/core/server/web/api/endpoints/admin/routes.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • ghost/core/core/server/web/api/endpoints/admin/routes.js
  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-30T17:13:26.190Z
Learnt from: sam-lord
Repo: TryGhost/Ghost PR: 25303
File: ghost/core/core/server/services/email-service/BatchSendingService.js:19-19
Timestamp: 2025-10-30T17:13:26.190Z
Learning: In ghost/core/core/server/services/email-service/BatchSendingService.js and similar files in the Ghost codebase, prefer using `{...options}` spread syntax without explicit guards like `...(options || {})` when spreading potentially undefined objects, as the maintainer prefers cleaner syntax over defensive patterns when the behavior is safe.

Applied to files:

  • ghost/core/core/server/services/member-welcome-emails/service.js
🧬 Code graph analysis (2)
apps/admin-x-framework/src/api/automated-emails.ts (1)
apps/admin-x-framework/src/utils/api/hooks.ts (1)
  • createMutation (184-215)
ghost/core/core/server/services/member-welcome-emails/service.js (1)
ghost/core/core/server/services/member-welcome-emails/constants.js (2)
  • MEMBER_WELCOME_EMAIL_LOG_KEY (1-1)
  • MESSAGES (8-13)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Admin-X Settings tests
  • GitHub Check: Ghost-CLI tests
  • GitHub Check: Acceptance tests (Node 22.18.0, mysql8)
  • GitHub Check: Legacy tests (Node 22.18.0, mysql8)
  • GitHub Check: Acceptance tests (Node 22.18.0, sqlite3)
  • GitHub Check: Unit tests (Node 22.18.0)
  • GitHub Check: Build & Push Docker Image
🔇 Additional comments (3)
ghost/core/core/server/services/member-welcome-emails/service.js (1)

105-144: Well-structured test email implementation.

The method follows the existing patterns in the service, with appropriate logging, error handling, and the [Test] subject prefix to clearly identify test emails. The hardcoded test member name "Jamie Larson" is a reasonable approach for previews.

apps/admin-x-framework/src/api/automated-emails.ts (1)

52-56: LGTM!

The mutation hook correctly follows the established patterns in this file. Using unknown for the response type is appropriate since the endpoint returns 204 No Content, and omitting cache invalidation is correct as test emails don't modify any data.

ghost/core/core/server/api/endpoints/automated-emails.js (1)

142-148: Service initialization pattern is correct.

The init() call before accessing api.sendTestEmail() follows the wrapper pattern used throughout Ghost services, ensuring the service instance is created before use.

@troyciesco troyciesco force-pushed the NY-788_send-test-welcome-emails branch from 56087c9 to 18e8dc5 Compare December 10, 2025 23:29
@troyciesco troyciesco removed the migration [pull request] Includes migration for review label Dec 10, 2025
@TryGhost TryGhost deleted a comment from github-actions bot Dec 10, 2025
@troyciesco troyciesco force-pushed the NY-788_send-test-welcome-emails branch 2 times, most recently from 1a4edeb to ac0258d Compare December 11, 2025 00:09
@troyciesco troyciesco requested a review from cmraible December 11, 2025 00:27
@troyciesco troyciesco force-pushed the NY-788_send-test-welcome-emails branch from ac0258d to 5e3dc97 Compare December 11, 2025 15:52
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
ghost/core/core/server/services/member-welcome-emails/service.js (1)

105-144: Add a defensive check for missing lexical before rendering test emails

The new sendTestEmail flow looks good overall: it reuses the existing renderer/mailer pipeline, logs clearly, and prefixes subjects with [Test] to distinguish these messages.

There are two edge cases worth tightening up:

  1. Missing lexical content (duplicate of earlier review concern)
    You read const lexical = automatedEmail.get('lexical'); and pass it straight into this.#renderer.render(...). If lexical is null/empty, the renderer may fail or produce confusing output. Adding a simple guard here (and returning a clear ValidationError/IncorrectUsageError message) would make failures deterministic and easier to debug.

  2. Non–welcome automated emails
    AutomatedEmail.findOne({id: automatedEmailId}) does not verify that the record’s slug/type matches one of MEMBER_WELCOME_EMAIL_SLUGS. That means any automated email id could be used with the member‑welcome renderer. If that’s not intended, consider asserting the slug is one of the known welcome‑email slugs and throwing a NotFoundError (or a more specific error) otherwise.

Both of these are relatively small guards that would make the endpoint more robust without changing the main behavior.

🧹 Nitpick comments (1)
ghost/core/core/server/api/endpoints/utils/validators/input/automated_emails.js (1)

4-5: Email validation for sendTestEmail is correctly moved into the custom validator; consider error type consistency

The sendTestEmail validator doing typeof email === 'string' && validator.isEmail(email) and throwing on failure is aligned with how other endpoints handle email format checks and matches the earlier review direction to keep this logic in the validator module.

One minor point to double‑check: other validations in this file use ValidationError, while this path uses BadRequestError. If the API layer or clients rely on consistent error classes for input issues, you may want to standardize on one of them here (or confirm that BadRequestError is the intended distinction for this endpoint).

Also applies to: 16-18, 69-77

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ac0258d and 5e3dc97.

⛔ Files ignored due to path filters (1)
  • ghost/core/test/e2e-api/admin/__snapshots__/automated-emails.test.js.snap is excluded by !**/*.snap
📒 Files selected for processing (7)
  • apps/admin-x-framework/src/api/automated-emails.ts (1 hunks)
  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx (4 hunks)
  • ghost/core/core/server/api/endpoints/automated-emails.js (2 hunks)
  • ghost/core/core/server/api/endpoints/utils/validators/input/automated_emails.js (3 hunks)
  • ghost/core/core/server/services/member-welcome-emails/service.js (1 hunks)
  • ghost/core/core/server/web/api/endpoints/admin/routes.js (1 hunks)
  • ghost/core/test/e2e-api/admin/automated-emails.test.js (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • ghost/core/test/e2e-api/admin/automated-emails.test.js
  • apps/admin-x-framework/src/api/automated-emails.ts
  • ghost/core/core/server/web/api/endpoints/admin/routes.js
  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-10-30T17:13:26.190Z
Learnt from: sam-lord
Repo: TryGhost/Ghost PR: 25303
File: ghost/core/core/server/services/email-service/BatchSendingService.js:19-19
Timestamp: 2025-10-30T17:13:26.190Z
Learning: In ghost/core/core/server/services/email-service/BatchSendingService.js and similar files in the Ghost codebase, prefer using `{...options}` spread syntax without explicit guards like `...(options || {})` when spreading potentially undefined objects, as the maintainer prefers cleaner syntax over defensive patterns when the behavior is safe.

Applied to files:

  • ghost/core/core/server/services/member-welcome-emails/service.js
  • ghost/core/core/server/api/endpoints/automated-emails.js
📚 Learning: 2025-11-10T23:10:17.470Z
Learnt from: troyciesco
Repo: TryGhost/Ghost PR: 25288
File: ghost/core/core/server/services/member-welcome-emails/jobs/lib/process-entries.js:46-64
Timestamp: 2025-11-10T23:10:17.470Z
Learning: In ghost/core/core/server/services/member-welcome-emails/jobs/lib/process-entries.js and the outbox processing flow, entries are marked as PROCESSING before being processed. If a failure occurs after email send but before deletion, the entry remains stuck in PROCESSING state (not reprocessed). This intentional design prevents duplicate emails. Handling stuck PROCESSING entries is planned for a separate PR.

Applied to files:

  • ghost/core/core/server/api/endpoints/automated-emails.js
📚 Learning: 2025-08-11T19:39:00.428Z
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 24651
File: ghost/core/test/utils/urlUtils.js:53-57
Timestamp: 2025-08-11T19:39:00.428Z
Learning: In Ghost's test utilities, when fixing specific issues like async behavior, it's preferred to maintain existing error handling patterns (even if suboptimal) to keep PRs focused on their primary objective. Error handling improvements can be addressed in separate, dedicated PRs.

Applied to files:

  • ghost/core/core/server/api/endpoints/automated-emails.js
📚 Learning: 2025-06-13T11:57:58.226Z
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 23824
File: ghost/core/core/server/services/email-service/email-templates/partials/styles.hbs:919-926
Timestamp: 2025-06-13T11:57:58.226Z
Learning: In `ghost/core/core/server/services/email-service/email-templates/partials/styles.hbs`, some style blocks (e.g., `.latest-post p` and `.latest-post p a`) still use the legacy colour `#73818c` on purpose; they are later overridden by `emailCustomization` feature rules, as noted by inline TODO comments. These occurrences should not be flagged as inconsistencies.

Applied to files:

  • ghost/core/core/server/api/endpoints/automated-emails.js
📚 Learning: 2025-07-09T18:06:09.856Z
Learnt from: troyciesco
Repo: TryGhost/Ghost PR: 24291
File: apps/admin-x-settings/package.json:45-45
Timestamp: 2025-07-09T18:06:09.856Z
Learning: In Ghost admin-x-settings, webhook URL validation uses {require_tld: false} to allow localhost URLs for local integration testing, while social URL validation uses the default {require_tld: true} since social URLs should be public-facing.

Applied to files:

  • ghost/core/core/server/api/endpoints/automated-emails.js
🧬 Code graph analysis (2)
ghost/core/core/server/api/endpoints/utils/validators/input/automated_emails.js (1)
ghost/core/core/server/api/endpoints/automated-emails.js (2)
  • tpl (1-1)
  • messages (6-8)
ghost/core/core/server/api/endpoints/automated-emails.js (3)
ghost/core/core/server/api/endpoints/utils/validators/input/automated_emails.js (1)
  • require (5-5)
ghost/core/core/server/services/member-welcome-emails/service.js (2)
  • require (9-9)
  • require (11-11)
ghost/core/core/server/web/api/endpoints/admin/routes.js (1)
  • require (3-3)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Legacy tests (Node 22.18.0, mysql8)
  • GitHub Check: Legacy tests (Node 22.18.0, sqlite3)
  • GitHub Check: Lint
  • GitHub Check: Ghost-CLI tests
  • GitHub Check: Admin-X Settings tests
  • GitHub Check: ActivityPub tests
  • GitHub Check: Unit tests (Node 22.18.0)
  • GitHub Check: Build & Push Docker Image
🔇 Additional comments (1)
ghost/core/core/server/api/endpoints/automated-emails.js (1)

4-4: Endpoint wiring for sendTestEmail looks consistent with existing patterns

The new sendTestEmail controller action is wired cleanly:

  • Uses statusCode: 204 with no cache, matching other “fire‑and‑forget” endpoints.
  • Accepts options: ['id'] and data: ['email'], with id required and email validation delegated to the automated_emails input validator.
  • Initializes the memberWelcomeEmailService wrapper before calling api.sendTestEmail({ email, automatedEmailId: frame.options.id }), which matches how other services are used in this layer.
  • Permissions piggy‑back on the edit method, in line with the stated intent that editing and sending test welcome emails are coupled for admins.

No issues from a controller/API‑layer perspective.

Also applies to: 116-143

no ref

Original PR first saves the automated email before sending the test
email, but that creates a scenario where you'll _also_ have the changes
live, which you might not want.

This PR, pointed at that one for comparison, switches the logic around
so whatever is currently in the form for subject and lexical is what
gets sent. This also happens to make validation and
error/loading/success states a lot less jumpy.
@troyciesco troyciesco force-pushed the NY-788_send-test-welcome-emails branch from 04cc23f to a45ccd5 Compare December 15, 2025 14:53
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
ghost/core/core/server/api/endpoints/utils/validators/input/automated_emails.js (1)

4-20: Test-email validator is solid; consider also enforcing lexical JSON validity

The new sendTestEmail validator cleanly enforces:

  • Well-formed email (via isEmail).
  • Non-empty subject.
  • Non-empty lexical.

That’s a good baseline.

One improvement to keep behavior aligned with add/edit and to avoid renderer-time failures: also ensure lexical is valid JSON here, so malformed payloads result in a clear 400 error instead of bubbling up as a 500 when the renderer parses it.

For example:

     if (typeof lexical !== 'string' || !lexical.trim()) {
         throw new BadRequestError({
             message: tpl(messages.lexicalRequired)
         });
     }
+
+    try {
+        JSON.parse(lexical);
+    } catch (e) {
+        throw new BadRequestError({
+            message: tpl(messages.invalidLexical)
+        });
+    }

This keeps all input-shape validation in one place and makes the test endpoint more robust against bad clients.

Also applies to: 70-93

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5e3dc97 and a45ccd5.

⛔ Files ignored due to path filters (1)
  • ghost/core/test/e2e-api/admin/__snapshots__/automated-emails.test.js.snap is excluded by !**/*.snap
📒 Files selected for processing (7)
  • apps/admin-x-framework/src/api/automated-emails.ts (1 hunks)
  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx (4 hunks)
  • ghost/core/core/server/api/endpoints/automated-emails.js (2 hunks)
  • ghost/core/core/server/api/endpoints/utils/validators/input/automated_emails.js (3 hunks)
  • ghost/core/core/server/services/member-welcome-emails/service.js (1 hunks)
  • ghost/core/core/server/web/api/endpoints/admin/routes.js (1 hunks)
  • ghost/core/test/e2e-api/admin/automated-emails.test.js (3 hunks)
🧰 Additional context used
🧠 Learnings (19)
📚 Learning: 2025-11-25T14:28:50.351Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T14:28:50.351Z
Learning: API routes should be defined in `ghost/core/core/server/api/`

Applied to files:

  • ghost/core/core/server/web/api/endpoints/admin/routes.js
📚 Learning: 2025-10-30T17:13:26.190Z
Learnt from: sam-lord
Repo: TryGhost/Ghost PR: 25303
File: ghost/core/core/server/services/email-service/BatchSendingService.js:19-19
Timestamp: 2025-10-30T17:13:26.190Z
Learning: In ghost/core/core/server/services/email-service/BatchSendingService.js and similar files in the Ghost codebase, prefer using `{...options}` spread syntax without explicit guards like `...(options || {})` when spreading potentially undefined objects, as the maintainer prefers cleaner syntax over defensive patterns when the behavior is safe.

Applied to files:

  • ghost/core/core/server/services/member-welcome-emails/service.js
  • ghost/core/core/server/api/endpoints/automated-emails.js
  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.test.ts : Test suite names should follow the format 'Ghost Admin - Feature' or 'Ghost Public - Feature'

Applied to files:

  • ghost/core/test/e2e-api/admin/automated-emails.test.js
📚 Learning: 2025-03-13T09:00:20.205Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/utils/pending-activity.ts:13-71
Timestamp: 2025-03-13T09:00:20.205Z
Learning: The pending activity utilities in the Ghost ActivityPub module are covered by tests in the file `apps/admin-x-activitypub/test/unit/utils/pending-activity.ts`.

Applied to files:

  • ghost/core/test/e2e-api/admin/automated-emails.test.js
📚 Learning: 2025-11-25T14:28:50.351Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T14:28:50.351Z
Learning: Run `yarn test:integration` for integration tests in ghost/core

Applied to files:

  • ghost/core/test/e2e-api/admin/automated-emails.test.js
📚 Learning: 2025-03-13T09:00:20.205Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/utils/pending-activity.ts:13-71
Timestamp: 2025-03-13T09:00:20.205Z
Learning: The pending activity utilities in Ghost's ActivityPub module are thoroughly tested in `apps/admin-x-activitypub/test/unit/utils/pending-activity.ts`, which covers `generatePendingActivityId`, `isPendingActivity`, and `generatePendingActivity` functions.

Applied to files:

  • ghost/core/test/e2e-api/admin/automated-emails.test.js
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Ensure tests have fresh Ghost instance isolation (automatic) and do not create test dependencies where Test B needs Test A

Applied to files:

  • ghost/core/test/e2e-api/admin/automated-emails.test.js
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Each test receives fresh Ghost instance for automatic isolation

Applied to files:

  • ghost/core/test/e2e-api/admin/automated-emails.test.js
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.test.ts : Use factory pattern for all test data creation instead of hard-coded data or direct database manipulation

Applied to files:

  • ghost/core/test/e2e-api/admin/automated-emails.test.js
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.test.ts : Manual login should not be used - authentication is automatic via fixture

Applied to files:

  • ghost/core/test/e2e-api/admin/automated-emails.test.js
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.test.ts : Use Playwright's auto-waiting capabilities and run tests multiple times to ensure stability

Applied to files:

  • ghost/core/test/e2e-api/admin/automated-emails.test.js
📚 Learning: 2025-11-25T14:28:50.351Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T14:28:50.351Z
Learning: Applies to e2e/**/*.{ts,js} : E2E tests should use Playwright with Docker container isolation

Applied to files:

  • ghost/core/test/e2e-api/admin/automated-emails.test.js
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Always refer to `.claude/E2E_TEST_WRITING_GUIDE.md` for comprehensive testing guidelines and patterns when creating or modifying E2E tests

Applied to files:

  • ghost/core/test/e2e-api/admin/automated-emails.test.js
📚 Learning: 2025-08-11T19:39:00.428Z
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 24651
File: ghost/core/test/utils/urlUtils.js:53-57
Timestamp: 2025-08-11T19:39:00.428Z
Learning: In Ghost's test utilities, when fixing specific issues like async behavior, it's preferred to maintain existing error handling patterns (even if suboptimal) to keep PRs focused on their primary objective. Error handling improvements can be addressed in separate, dedicated PRs.

Applied to files:

  • ghost/core/core/server/api/endpoints/automated-emails.js
  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx
📚 Learning: 2025-11-10T23:10:17.470Z
Learnt from: troyciesco
Repo: TryGhost/Ghost PR: 25288
File: ghost/core/core/server/services/member-welcome-emails/jobs/lib/process-entries.js:46-64
Timestamp: 2025-11-10T23:10:17.470Z
Learning: In ghost/core/core/server/services/member-welcome-emails/jobs/lib/process-entries.js and the outbox processing flow, entries are marked as PROCESSING before being processed. If a failure occurs after email send but before deletion, the entry remains stuck in PROCESSING state (not reprocessed). This intentional design prevents duplicate emails. Handling stuck PROCESSING entries is planned for a separate PR.

Applied to files:

  • ghost/core/core/server/api/endpoints/automated-emails.js
  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx
📚 Learning: 2025-06-13T11:57:58.226Z
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 23824
File: ghost/core/core/server/services/email-service/email-templates/partials/styles.hbs:919-926
Timestamp: 2025-06-13T11:57:58.226Z
Learning: In `ghost/core/core/server/services/email-service/email-templates/partials/styles.hbs`, some style blocks (e.g., `.latest-post p` and `.latest-post p a`) still use the legacy colour `#73818c` on purpose; they are later overridden by `emailCustomization` feature rules, as noted by inline TODO comments. These occurrences should not be flagged as inconsistencies.

Applied to files:

  • ghost/core/core/server/api/endpoints/automated-emails.js
📚 Learning: 2025-07-09T18:06:09.856Z
Learnt from: troyciesco
Repo: TryGhost/Ghost PR: 24291
File: apps/admin-x-settings/package.json:45-45
Timestamp: 2025-07-09T18:06:09.856Z
Learning: In Ghost admin-x-settings, webhook URL validation uses {require_tld: false} to allow localhost URLs for local integration testing, while social URL validation uses the default {require_tld: true} since social URLs should be public-facing.

Applied to files:

  • ghost/core/core/server/api/endpoints/automated-emails.js
📚 Learning: 2025-11-10T11:30:41.316Z
Learnt from: rob-ghost
Repo: TryGhost/Ghost PR: 25357
File: apps/admin/test-utils/test-helpers.ts:6-12
Timestamp: 2025-11-10T11:30:41.316Z
Learning: In apps/admin/test-utils/test-helpers.ts, the waitForQuerySettled helper is intentionally designed to timeout for idle/disabled queries. It should only treat queries as settled when they reach a terminal state (isSuccess or isError) and are not fetching. This ensures tests properly wait for active queries to complete.

Applied to files:

  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.test.ts : Never use hard-coded waits like `waitForTimeout()`

Applied to files:

  • apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx
🧬 Code graph analysis (4)
ghost/core/core/server/web/api/endpoints/admin/routes.js (2)
ghost/core/core/server/web/api/endpoints/content/routes.js (3)
  • router (12-12)
  • mw (5-5)
  • api (3-3)
ghost/core/core/server/web/api/endpoints/admin/middleware.js (1)
  • shared (4-4)
ghost/core/test/e2e-api/admin/automated-emails.test.js (3)
ghost/core/core/server/api/endpoints/utils/validators/input/automated_emails.js (1)
  • require (5-5)
ghost/core/core/server/services/member-welcome-emails/service.js (2)
  • require (9-9)
  • require (11-11)
ghost/core/core/server/web/api/endpoints/admin/routes.js (1)
  • require (3-3)
apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx (2)
apps/admin-x-framework/src/api/automated-emails.ts (1)
  • useSendTestWelcomeEmail (52-56)
ghost/core/core/server/api/endpoints/utils/validators/input/automated_emails.js (1)
  • validator (4-4)
apps/admin-x-framework/src/api/automated-emails.ts (1)
apps/admin-x-framework/src/utils/api/hooks.ts (1)
  • createMutation (184-215)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: ActivityPub tests
  • GitHub Check: Legacy tests (Node 22.18.0, mysql8)
  • GitHub Check: Ghost-CLI tests
  • GitHub Check: Admin-X Settings tests
  • GitHub Check: Legacy tests (Node 22.18.0, sqlite3)
  • GitHub Check: Unit tests (Node 22.18.0)
  • GitHub Check: Lint
  • GitHub Check: Acceptance tests (Node 22.18.0, sqlite3)
  • GitHub Check: Acceptance tests (Node 22.18.0, mysql8)
  • GitHub Check: Build & Push Docker Image
🔇 Additional comments (6)
ghost/core/core/server/web/api/endpoints/admin/routes.js (1)

182-188: New automated email test route wiring looks correct

The POST /automated_emails/:id/test route is wired consistently with other email test/preview routes (rate-limited via previewEmailLimiter and guarded by authAdminApi), and correctly delegates to api.automatedEmails.sendTestEmail. No issues from the routing/security perspective.

ghost/core/core/server/services/member-welcome-emails/service.js (1)

105-142: sendTestEmail implementation is consistent with existing welcome-email flow

The sendTestEmail method correctly:

  • Verifies the AutomatedEmail exists (404 otherwise).
  • Reuses the same siteSettings pattern as the main send path.
  • Renders using the caller-supplied lexical and subject for a synthetic test member.
  • Sends to the provided address with a [Test] subject prefix.

This matches the intended behavior for testing unsaved welcome email content without impacting real members.

ghost/core/core/server/api/endpoints/automated-emails.js (1)

4-5: Controller-level wiring for sendTestEmail is sound

The new sendTestEmail endpoint:

  • Uses a 204 response with cacheInvalidate: false, consistent with other “test/send-only” actions.
  • Requires options.id and passes it through as automatedEmailId.
  • Delegates all business logic to memberWelcomeEmailService.api.sendTestEmail after initialization.
  • Uses {method: 'edit'} permissions, aligning with the PR’s intent that edit + sendTestEmail are coupled for member welcome emails.

No issues spotted here.

Also applies to: 116-147

ghost/core/test/e2e-api/admin/automated-emails.test.js (1)

3-5: E2E coverage for SendTestEmail is comprehensive and isolation-safe

  • Truncating brute and automated_emails in beforeEach keeps the rate-limiter/table state clean between tests.
  • Stubbing GhostMailer.prototype.send avoids real email sends while still exercising the end-to-end flow.
  • The SendTestEmail suite covers:
    • 204 success for a valid request.
    • 404 for non-existent automated email id.
    • 400s for missing email, invalid email, missing subject, and missing lexical.

This gives solid confidence in the new endpoint’s behavior.

Also applies to: 36-39, 320-472

apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx (1)

2-3: Test-welcome-email UI flow is well-integrated and handles state/cleanup correctly

  • Client-side checks (email via validator.isEmail and useForm’s subject/lexical validation) prevent most bad requests before hitting the API.
  • sendState + sendStateTimeoutRef give clear “sending/sent” feedback, and the unmount cleanup (useEffect with clearTimeout) avoids lingering timers or setState-on-unmounted issues.
  • Error handling maps JSON API errors (including validation messages from the new endpoint) into concise user-visible copy, falling back to a generic message when needed.
  • The dropdown, test email field, and Send button are wired cleanly into the existing modal without interfering with the main save flow.

No changes needed here.

Also applies to: 9-13, 47-58, 62-85, 87-93, 95-109, 116-127, 129-167, 187-216

apps/admin-x-framework/src/api/automated-emails.ts (1)

52-56: API hook matches backend contract for test welcome emails

useSendTestWelcomeEmail correctly:

  • Targets /automated_emails/{id}/test/ with method POST.
  • Sends {email, subject, lexical} in the body, matching the server’s expected payload.
  • Uses unknown response type, appropriate for a 204/empty response.

Hook signature lines up with the modal’s usage and the new backend endpoint.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants