Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
const path = require('path');
const jobsService = require('../../jobs');
const labs = require('../../../../shared/labs');
const config = require('../../../../shared/config');

let hasScheduled = {
processOutbox: false
};

module.exports = {
async scheduleMemberWelcomeEmailJob() {
if (!labs.isSet('welcomeEmails')) {
if (!config.get('memberWelcomeEmailTestInbox')) {
return false;
}

if (!hasScheduled.processOutbox && !process.env.NODE_ENV.startsWith('test')) {
if (hasScheduled.processOutbox && !process.env.NODE_ENV.startsWith('test')) {

Choose a reason for hiding this comment

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

🚨 Bug: Inverted guard condition prevents job from ever being scheduled

The negation operator ! was removed from the hasScheduled.processOutbox check during the refactor. The original code was:

if (!hasScheduled.processOutbox && !process.env.NODE_ENV.startsWith('test'))

The new code is:

if (hasScheduled.processOutbox && !process.env.NODE_ENV.startsWith('test'))

Since hasScheduled.processOutbox is initialized to false, the condition hasScheduled.processOutbox && ... will always be false on the first call, so jobsService.addJob() is never invoked and hasScheduled.processOutbox is never set to true. The welcome email cron job will never be scheduled, making the entire feature non-functional in production.

This is a regression — the ! must be restored.

Was this helpful? React with 👍 / 👎

Suggested change
if (hasScheduled.processOutbox && !process.env.NODE_ENV.startsWith('test')) {
if (!hasScheduled.processOutbox && !process.env.NODE_ENV.startsWith('test')) {
  • Apply suggested fix

jobsService.addJob({
at: '0 */5 * * * *',
job: path.resolve(__dirname, 'process-outbox.js'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -336,8 +337,9 @@ module.exports = class MemberRepository {
const eventData = _.pick(data, ['created_at']);

const memberAddOptions = {...(options || {}), withRelated};
let member;
if (this._labsService.isSet('welcomeEmails') && WELCOME_EMAIL_SOURCES.includes(source)) {
var member;
const welcomeEmailConfig = config.get('memberWelcomeEmailTestInbox');
if (welcomeEmailConfig || WELCOME_EMAIL_SOURCES.includes(source)) {

Choose a reason for hiding this comment

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

🚨 Bug: || should be && — welcome emails sent even when disabled

The original condition required both the feature flag to be enabled AND the source to be in the allowed list:

if (this._labsService.isSet('welcomeEmails') && WELCOME_EMAIL_SOURCES.includes(source))

The new code uses || (OR) instead of && (AND):

if (welcomeEmailConfig || WELCOME_EMAIL_SOURCES.includes(source))

This means:

  1. When welcomeEmailConfig is truthy, outbox entries are created for all sources (including 'import', 'admin', etc.) — not just the allowed ones.
  2. When welcomeEmailConfig is falsy but the source is 'member', outbox entries are still created — the feature can never be fully disabled.

Both branches are incorrect. The operator should remain && to preserve the original semantics: welcome emails are sent only when the config is set and the source is in the allowed list.

Was this helpful? React with 👍 / 👎

Suggested change
if (welcomeEmailConfig || WELCOME_EMAIL_SOURCES.includes(source)) {
if (welcomeEmailConfig && WELCOME_EMAIL_SOURCES.includes(source)) {
  • Apply suggested fix

const runMemberCreation = async (transacting) => {
const newMember = await this._Member.add({
...memberData,
Expand Down
19 changes: 9 additions & 10 deletions ghost/core/test/integration/services/member-welcome-emails.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +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 sinon = require('sinon');
const configUtils = require('../../utils/configUtils');

describe('Member Welcome Emails Integration', function () {
let membersService;
Expand All @@ -22,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 () {
sinon.stub(labs, 'isSet').withArgs('welcomeEmails').returns(true);
configUtils.set('memberWelcomeEmailTestInbox', 'test-inbox@example.com');

const member = await membersService.api.members.create({
email: 'welcome-test@example.com',
Expand All @@ -50,9 +49,9 @@ 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 () {
configUtils.set('memberWelcomeEmailTestInbox', '');
await membersService.api.members.create({
email: 'no-welcome@example.com',
name: 'No Welcome Member'
Expand All @@ -66,7 +65,7 @@ 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);
configUtils.set('memberWelcomeEmailTestInbox', 'test-inbox@example.com');

await membersService.api.members.create({
email: 'imported@example.com',
Expand All @@ -81,7 +80,7 @@ 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);
configUtils.set('memberWelcomeEmailTestInbox', 'test-inbox@example.com');

await membersService.api.members.create({
email: 'admin-created@example.com',
Expand All @@ -96,7 +95,7 @@ describe('Member Welcome Emails Integration', function () {
});

it('creates outbox entry with correct timestamp', async function () {
sinon.stub(labs, 'isSet').withArgs('welcomeEmails').returns(true);
configUtils.set('memberWelcomeEmailTestInbox', 'test-inbox@example.com');

const beforeCreation = new Date(Date.now() - 1000);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -466,7 +467,6 @@ describe('MemberRepository', function () {
let Outbox;
let MemberStatusEvent;
let MemberSubscribeEvent;
let labsService;
let newslettersService;
const oldNodeEnv = process.env.NODE_ENV;

Expand Down Expand Up @@ -527,16 +527,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
});
Expand All @@ -554,17 +551,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
});
Expand All @@ -575,16 +569,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
});
Expand All @@ -603,16 +594,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
});
Expand All @@ -625,16 +613,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
});
Expand Down