From 4bbdd6f9ab43262b93152912b1d940bb7539535a Mon Sep 17 00:00:00 2001 From: Troy Ciesco Date: Mon, 17 Nov 2025 15:48:40 -0500 Subject: [PATCH 1/5] 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 a256326703aeb1a4a112aad42c6eb9173411d3b7 Mon Sep 17 00:00:00 2001 From: Troy Ciesco Date: Mon, 17 Nov 2025 16:25:33 -0500 Subject: [PATCH 2/5] 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 ad752c68707c4bfd5f3a8d676cd3108ba0b3d07c Mon Sep 17 00:00:00 2001 From: Troy Ciesco Date: Mon, 17 Nov 2025 16:30:51 -0500 Subject: [PATCH 3/5] 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 81e6ddc7283b6be1157bce58dc6c0106d113e183 Mon Sep 17 00:00:00 2001 From: Troy Ciesco Date: Mon, 17 Nov 2025 16:47:53 -0500 Subject: [PATCH 4/5] 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 11cc5e9759d1445d9b5afa03006400052f3908be Mon Sep 17 00:00:00 2001 From: Troy Ciesco Date: Mon, 17 Nov 2025 17:23:27 -0500 Subject: [PATCH 5/5] 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);