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')) {
jobsService.addJob({
at: '0 */5 * * * *',
job: path.resolve(__dirname, 'process-outbox.js'),
Comment on lines +15 to 18

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Job never scheduled 🐞 Bug ✓ Correctness

scheduleMemberWelcomeEmailJob() now checks hasScheduled.processOutbox instead of
  !hasScheduled.processOutbox, but the flag initializes to false and is only set to true inside
  the guarded block.
• This makes the scheduling block unreachable on first run, so the recurring outbox-processing job
  is never added (welcome emails won’t be processed).
• If the flag were ever toggled elsewhere, the current logic would also allow duplicate scheduling
  on repeated calls instead of preventing it.
Agent prompt
### Issue description
The member welcome email outbox processor job is never scheduled because the `hasScheduled.processOutbox` check was inverted.

### Issue Context
`hasScheduled.processOutbox` initializes to `false` and is only set to `true` inside the guarded block. With the current condition, the guarded block is unreachable.

### Fix Focus Areas
- ghost/core/core/server/services/member-welcome-emails/jobs/index.js[15-23]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. var member declaration used 📘 Rule violation ✓ Correctness

• New code declares member with var, which violates the requirement to use block-scoped
  let/const.
• This increases the risk of hoisting/scope-related bugs and makes the variable’s lifetime harder to
  reason about.
Agent prompt
## Issue description
`MemberRepository.js` introduces `var member;`, which violates the codebase requirement to use `let`/`const` instead of `var`.

## Issue Context
`var` is function-scoped and hoisted, which can lead to subtle scoping/initialization issues.

## Fix Focus Areas
- ghost/core/core/server/services/members/members-api/repositories/MemberRepository.js[339-342]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

const welcomeEmailConfig = config.get('memberWelcomeEmailTestInbox');
if (welcomeEmailConfig || WELCOME_EMAIL_SOURCES.includes(source)) {
Comment on lines +341 to +342

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Config check uses wrong or 📘 Rule violation ⛯ Reliability

• The new condition if (welcomeEmailConfig || WELCOME_EMAIL_SOURCES.includes(source)) will
  evaluate true for source === 'member' even when the config is unset/undefined.
• This fails to handle the “config not set” edge case, potentially creating outbox entries (and
  sending welcome emails) when the feature should be disabled.
Agent prompt
## Issue description
The welcome-email gating logic uses `||`, which bypasses the "config not set" edge case. Because `WELCOME_EMAIL_SOURCES` contains `'member'`, the condition becomes true even when `config.get('memberWelcomeEmailTestInbox')` is undefined/empty.

## Issue Context
This can cause outbox entries to be created (and downstream welcome email processing to run) when configuration indicates the feature should be disabled.

## Fix Focus Areas
- ghost/core/core/server/services/members/members-api/repositories/MemberRepository.js[340-342]
- ghost/core/core/server/services/members/members-api/repositories/MemberRepository.js[26-29]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

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