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'),
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.

⚠️ Potential issue | 🟡 Minor

Use let instead of var for block-scoped variable.

ESLint flags this as a violation of the no-var rule.

Suggested fix
-        var member;
+        let member;
📝 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
var member;
let member;
🧰 Tools
🪛 ESLint

[error] 340-340: Unexpected var, use let or const instead.

(no-var)

🤖 Prompt for AI Agents
In
`@ghost/core/core/server/services/members/members-api/repositories/MemberRepository.js`
at line 340, Replace the function-scoped declaration "var member;" with a
block-scoped declaration using "let member;" (i.e., change the identifier
declared as var to let) so it adheres to the no-var ESLint rule; locate the "var
member" declaration in MemberRepository.js (the variable used later in this
method) and update it to "let" and run lint/tests to ensure no other scope
issues arise.

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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "MemberRepository.js" | head -5

Repository: qodo-benchmark/Ghost-combined-coderabbit

Length of output: 170


🏁 Script executed:

wc -l ghost/core/core/server/services/members/members-api/repositories/MemberRepository.js

Repository: qodo-benchmark/Ghost-combined-coderabbit

Length of output: 173


🏁 Script executed:

sed -n '330,355p' ghost/core/core/server/services/members/members-api/repositories/MemberRepository.js

Repository: qodo-benchmark/Ghost-combined-coderabbit

Length of output: 1182


🏁 Script executed:

# Find unit tests related to welcome email or this repository
find . -type f -name "*.test.js" -o -name "*.spec.js" | xargs grep -l "does not create outbox entry for disallowed sources" 2>/dev/null

Repository: qodo-benchmark/Ghost-combined-coderabbit

Length of output: 180


🏁 Script executed:

# Search for WELCOME_EMAIL_SOURCES definition
rg "WELCOME_EMAIL_SOURCES" --type js -B 2 -A 2

Repository: qodo-benchmark/Ghost-combined-coderabbit

Length of output: 1329


🏁 Script executed:

grep -n "does not create outbox entry for disallowed sources" ghost/core/test/unit/server/services/members/members-api/repositories/MemberRepository.test.js

Repository: qodo-benchmark/Ghost-combined-coderabbit

Length of output: 173


🏁 Script executed:

# Get context around that test
sed -n '1,3000p' ghost/core/test/unit/server/services/members/members-api/repositories/MemberRepository.test.js | grep -B 20 -A 40 "does not create outbox entry for disallowed sources"

Repository: qodo-benchmark/Ghost-combined-coderabbit

Length of output: 2492


🏁 Script executed:

# Look for tests that verify outbox entry IS created (positive case)
sed -n '540,575p' ghost/core/test/unit/server/services/members/members-api/repositories/MemberRepository.test.js

Repository: qodo-benchmark/Ghost-combined-coderabbit

Length of output: 1572


🏁 Script executed:

# Check how _resolveContextSource works to understand source resolution
rg "_resolveContextSource" ghost/core/core/server/services/members/members-api/repositories/MemberRepository.js -B 2 -A 8

Repository: qodo-benchmark/Ghost-combined-coderabbit

Length of output: 3812


🏁 Script executed:

# Check if there's any other context or comments explaining this feature
sed -n '320,380p' ghost/core/core/server/services/members/members-api/repositories/MemberRepository.js

Repository: qodo-benchmark/Ghost-combined-coderabbit

Length of output: 2451


🏁 Script executed:

# Check if there are other tests that create outbox when config IS set
grep -B 5 -A 15 "creates outbox entry\|creates an outbox" ghost/core/test/unit/server/services/members/members-api/repositories/MemberRepository.test.js | head -40

Repository: qodo-benchmark/Ghost-combined-coderabbit

Length of output: 862


Fix logic operator: || should be && to restrict outbox entries to allowed sources.

The condition welcomeEmailConfig || WELCOME_EMAIL_SOURCES.includes(source) incorrectly creates outbox entries whenever the config is set, regardless of source. This contradicts the unit test "does not create outbox entry for disallowed sources," which sets memberWelcomeEmailTestInbox but expects NO outbox entry for import/admin/api sources.

Change the operator to && so outbox entries are created only when BOTH the config is enabled AND the source is 'member':

🐛 Fix
         const welcomeEmailConfig = config.get('memberWelcomeEmailTestInbox');
-        if (welcomeEmailConfig || WELCOME_EMAIL_SOURCES.includes(source)) {
+        if (welcomeEmailConfig && WELCOME_EMAIL_SOURCES.includes(source)) {
📝 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 welcomeEmailConfig = config.get('memberWelcomeEmailTestInbox');
if (welcomeEmailConfig || WELCOME_EMAIL_SOURCES.includes(source)) {
const welcomeEmailConfig = config.get('memberWelcomeEmailTestInbox');
if (welcomeEmailConfig && WELCOME_EMAIL_SOURCES.includes(source)) {
🤖 Prompt for AI Agents
In
`@ghost/core/core/server/services/members/members-api/repositories/MemberRepository.js`
around lines 341 - 342, The condition that decides whether to create a
welcome-email outbox entry is using a logical OR and thus allows entries
whenever memberWelcomeEmailTestInbox is set regardless of event source; update
the check in MemberRepository.js so that it uses logical AND: require both
welcomeEmailConfig (memberWelcomeEmailTestInbox) to be truthy and
WELCOME_EMAIL_SOURCES.includes(source) to be true before creating the outbox
entry (the symbols to change are welcomeEmailConfig, WELCOME_EMAIL_SOURCES, and
source in the conditional that precedes the outbox creation).

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