Skip to content

Improved email verification trigger tests#27026

Merged
aileen merged 1 commit intomainfrom
gva-732-verification-flow-tests
Apr 1, 2026
Merged

Improved email verification trigger tests#27026
aileen merged 1 commit intomainfrom
gva-732-verification-flow-tests

Conversation

@aileen
Copy link
Copy Markdown
Member

@aileen aileen commented Mar 31, 2026

Summary

  • Extracted shared webhook mock setup into verification-webhook-test-utils.ts to replace duplicated nock/config boilerplate
  • Updated members e2e, batch-sending, and members-importer tests to use the new verification flow with the verificationFlow flag enabled
  • Added new members-importer test for large import verification under the new flow
  • Tidyings of theverification-trigger.test.js unit test to use shared helpers

ref GVA-732

Test plan

  • Verify members e2e admin webhook verification test passes
  • Verify batch-sending verification-required test passes
  • Verify new members-importer large import verification test passes
  • Confirm no regressions in existing verification tests

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 31, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a new test helper module exporting setupEmailVerificationUtils and restoreEmailVerificationUtils to configure email verification state, stub webhook endpoints, and capture webhook requests. Tests across e2e, integration, legacy importer, and unit suites were refactored to use these helpers (replacing direct configUtils/Settings edits), consolidate email-verification scenarios, and introduce a shared createMemberThroughApi helper. Verification trigger logic was adjusted to compute a currentImport count from event pagination and use it when comparing against thresholds and starting verification. Tests now explicitly set and restore verification state and assert webhook payloads.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Improved email verification trigger tests' clearly reflects the main focus of the PR: refactoring and enhancing test infrastructure for email verification triggers, which aligns with the significant changes to test files and verification-trigger utilities across the changeset.
Description check ✅ Passed The PR description clearly relates to the changeset, explaining the extraction of webhook mock setup, updates to verification flow tests, and new test additions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gva-732-verification-flow-tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@aileen aileen force-pushed the gva-732-verification-flow-tests branch from be32a18 to a3e529f Compare March 31, 2026 10:07
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
ghost/core/test/legacy/api/admin/members-importer.test.js (1)

356-395: Add cleanup for nock interceptor to match other tests.

This test creates a nock interceptor via setupMockVerificationWebhook but doesn't clean it up. For consistency with members.test.js (which uses try/finally with nock.cleanAll()), consider adding cleanup to prevent potential test pollution.

♻️ Suggested cleanup pattern
     it('Can import members with host emailVerification limits for large imports - new flow', async function () {
         mockManager.mockLabsEnabled('verificationFlow');

         await models.Settings.edit([{
             key: 'email_verification_required',
             value: false
         }], {context: {internal: true}});

         assert(!settingsCache.get('email_verification_required'), 'Email verification should not be required');

         // If this test fails, check if the total members that have been created with fixtures has increased a lot, and if required, increase the amount of imported members
         const {receivedWebhookRequests} = setupMockVerificationWebhook({
             apiThreshold: 2,
             adminThreshold: 2,
             importThreshold: 1 // note: this one isn't really used because (totalMembers - members_created_in_last_30_days) is larger and used instead
         });

+        try {
             const awaitCompletion = jobManager.awaitCompletion('members-import');

             const res = await request
                 .post(localUtils.API.getApiQuery(`members/upload/`))
                 .field('labels', ['new-global-label'])
                 .attach('membersfile', path.join(__dirname, '/../../../utils/fixtures/csv/valid-members-import-large-501.csv'))
                 .set('Origin', config.get('url'))
                 .expect('Content-Type', /json/)
                 .expect('Cache-Control', testUtils.cacheRules.private)
                 .expect(202);
             assert.equal(res.headers['x-cache-invalidate'], undefined);
             const jsonResponse = res.body;

             assertExists(jsonResponse);
             assertExists(jsonResponse.meta);

             // Wait for the job to finish
             await awaitCompletion;

             assert(!!settingsCache.get('email_verification_required'), 'Email verification should now be required');

             assert(receivedWebhookRequests.length > 0, 'Expected to receive webhook requests');
+        } finally {
+            nock.cleanAll();
+        }
     });

Note: You'll need to add const nock = require('nock'); to the imports if not already present.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghost/core/test/legacy/api/admin/members-importer.test.js` around lines 356 -
395, The test 'Can import members with host emailVerification limits for large
imports - new flow' creates a nock interceptor via setupMockVerificationWebhook
but doesn't clean it up; wrap the test body (the code that calls
setupMockVerificationWebhook, awaits awaitCompletion, and asserts
receivedWebhookRequests) in a try/finally where the finally calls
nock.cleanAll(), and add const nock = require('nock') to the test imports if
it's not already present so the nock.cleanAll() call is available to clear the
interceptor after the test.
ghost/core/test/integration/services/email-service/batch-sending.test.js (1)

505-557: Consider capturing the returned scope object for better test control.

The setupMockVerificationWebhook function creates and returns a nock scope that can be used to assert on interceptor behavior or manually manage cleanup. The current test doesn't capture this return value, limiting visibility and control. While the test framework's mockManager.restore() does call nock.cleanAll() in the afterEach hook, explicitly capturing and managing the scope would improve test clarity and allow for scope-specific assertions if needed.

♻️ Suggested improvement
     it('Cannot send an email if verification is required', async function () {
         // Update this test to check the new flow instead.
         mockManager.mockLabsEnabled('verificationFlow');
         // First enable import thresholds
-        setupMockVerificationWebhook({
+        const {scope} = setupMockVerificationWebhook({
             apiThreshold: 100,
             adminThreshold: 100,
             importThreshold: 100
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghost/core/test/integration/services/email-service/batch-sending.test.js`
around lines 505 - 557, The test currently calls
setupMockVerificationWebhook(...) but ignores its returned nock scope; capture
that return (e.g., const scope = setupMockVerificationWebhook(...)) so you can
assert on or explicitly clean that scope before finishing the test; update the
test around the calls to setupMockVerificationWebhook and ensure you call
scope.done() or scope.abort() as appropriate (in addition to existing
mockManager.mockLabsEnabled('verificationFlow') and configUtils.restore()) to
improve control and visibility of the interceptor behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@ghost/core/test/integration/services/email-service/batch-sending.test.js`:
- Around line 505-557: The test currently calls
setupMockVerificationWebhook(...) but ignores its returned nock scope; capture
that return (e.g., const scope = setupMockVerificationWebhook(...)) so you can
assert on or explicitly clean that scope before finishing the test; update the
test around the calls to setupMockVerificationWebhook and ensure you call
scope.done() or scope.abort() as appropriate (in addition to existing
mockManager.mockLabsEnabled('verificationFlow') and configUtils.restore()) to
improve control and visibility of the interceptor behavior.

In `@ghost/core/test/legacy/api/admin/members-importer.test.js`:
- Around line 356-395: The test 'Can import members with host emailVerification
limits for large imports - new flow' creates a nock interceptor via
setupMockVerificationWebhook but doesn't clean it up; wrap the test body (the
code that calls setupMockVerificationWebhook, awaits awaitCompletion, and
asserts receivedWebhookRequests) in a try/finally where the finally calls
nock.cleanAll(), and add const nock = require('nock') to the test imports if
it's not already present so the nock.cleanAll() call is available to clear the
interceptor after the test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 203cc5fd-0372-405a-92a1-1a212e0f6f9e

📥 Commits

Reviewing files that changed from the base of the PR and between d2ca5cc and a3e529f.

📒 Files selected for processing (4)
  • ghost/core/test/e2e-api/admin/members.test.js
  • ghost/core/test/integration/services/email-service/batch-sending.test.js
  • ghost/core/test/legacy/api/admin/members-importer.test.js
  • ghost/core/test/utils/verification-webhook-test-utils.ts

@aileen aileen changed the title Updated verification tests to use new verificationFlow labs flag Updated email verification flow tests Mar 31, 2026
@@ -502,13 +503,13 @@ describe.skip('Batch sending tests', function () {
});

it('Cannot send an email if verification is required', async function () {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The test is actually set to be skipped in the parent subscribe block, since those tests appear to be flaky. The changes to this test pass in isolation.

apiThreshold: 2,
adminThreshold: 2,
importThreshold: 1, // note: this one isn't really used because (totalMembers - members_created_in_last_30_days) is larger and used instead
verified: false,
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

verified is set to false by default by the test util.

// relies on that value being true already.
});

it('Can still import members once email verification is required but does not send email', async function () {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not a huge fan that this test relies on the data manipulation of the previous test, but since it's a legacy test, I didn't change more than needed.

await restoreEmailVerificationUtils();
});

it('Can import members with host emailVerification limits for large imports - webhook flow', async function () {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I will remove the email checking tests above with this one, when promoting the labs flag to GA

const webhookSecret = DEFAULT_WEBHOOK_SECRET;
const webhookEndpoint = new URL(webhookUrl);

await models.Settings.edit([{
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Using settingsCache.set didn't work for the tests. It didn't trigger the events and I wasn't able to successfully update the settings. The direct update of the Settings model was a pattern I saw used in other tests and adopted it here.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ghost/core/core/server/services/verification-trigger.js`:
- Around line 107-122: The bug is that _handleMemberCreatedEvent uses
events.data.length (only the current page) while testImportThreshold uses
events.meta.pagination.total, so imports larger than the default page size are
undercounted; update both _handleMemberCreatedEvent and/or getSignupEvents calls
to request all results explicitly instead of relying on default pagination—e.g.,
call getSignupEvents({}, {source: 'member', limit:
<explicit-all-or-large-limit>}) or switch _handleMemberCreatedEvent to use
events.meta.pagination.total so both paths count the same total and still
respect the comment about write-behind timing when deciding to call
_startVerificationProcess.

In `@ghost/core/test/utils/email-verification-utils.ts`:
- Around line 69-77: The webhook mock's body parsing (inside the .reply
callback) currently does JSON.parse(rawBody) which can throw on empty or
non-JSON payloads; update the parsing for parsedBody to safely attempt
JSON.parse(rawBody) inside a try/catch and, on error, fall back to using rawBody
(string) or an empty object so the helper doesn't throw and tests can assert
payload shape; target the parsedBody logic and the same requestBody/rawBody
variables in this function when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 509af433-207d-4380-a7be-31d8249a5a29

📥 Commits

Reviewing files that changed from the base of the PR and between a3e529f and abc5eee.

⛔ Files ignored due to path filters (1)
  • ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap is excluded by !**/*.snap
📒 Files selected for processing (5)
  • ghost/core/core/server/services/verification-trigger.js
  • ghost/core/test/e2e-api/admin/members.test.js
  • ghost/core/test/integration/services/email-service/batch-sending.test.js
  • ghost/core/test/legacy/api/admin/members-importer.test.js
  • ghost/core/test/utils/email-verification-utils.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • ghost/core/test/integration/services/email-service/batch-sending.test.js
  • ghost/core/test/legacy/api/admin/members-importer.test.js
  • ghost/core/test/e2e-api/admin/members.test.js

@aileen aileen requested a review from sam-lord March 31, 2026 17:46
@aileen aileen force-pushed the gva-732-verification-flow-tests branch from abc5eee to 5b44a64 Compare April 1, 2026 07:34
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
ghost/core/test/integration/services/email-service/batch-sending.test.js (2)

519-556: ⚠️ Potential issue | 🟡 Minor

Clean up the stub and helper state in finally.

This file does not call sinon.restore() in afterEach, and restoreEmailVerificationUtils() only runs on the happy path. If an assertion fails before the end of the test, both the getSignupEvents stub and the verification helper state leak into later cases in an already-flaky suite.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghost/core/test/integration/services/email-service/batch-sending.test.js`
around lines 519 - 556, The test leaks stubs/state because getSignupEvents is
stubbed and restoreEmailVerificationUtils() is only called on the happy path;
ensure cleanup always runs by adding a finally/afterEach cleanup that calls
sinon.restore() and restoreEmailVerificationUtils() (or calls
restoreEmailVerificationUtils() inside a try/finally around the test body) so
the sinon stub getSignupEvents and any verification helper state are always
restored even when assertions fail; locate the stub created via
sinon.stub(events, 'getSignupEvents') and the call to
restoreEmailVerificationUtils() and make their teardown unconditional.

519-525: ⚠️ Potential issue | 🟡 Minor

Return the full signup-events page shape here.

The new verification-trigger path reads the page payload when calculating import counts. This stub only returns meta.pagination.total, so the test no longer matches the repository contract and can start failing or bypassing the new count logic when this skipped suite is re-enabled.

🧩 Example fix
         const getSignupEvents = sinon.stub(events, 'getSignupEvents').resolves({
+            data: Array.from({length: 101}, () => ({})),
             meta: {
                 pagination: {
-                    total: 100000
+                    total: 101,
+                    limit: 101
                 }
             }
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghost/core/test/integration/services/email-service/batch-sending.test.js`
around lines 519 - 525, The stubbed getSignupEvents in the test only returns
meta.pagination.total but the verification-trigger path expects the full page
payload shape; update the sinon.stub for events.getSignupEvents to resolve with
a complete page object including properties like events (an array of event
objects matching the shape used in the code), meta.pagination (total, limit,
page, pages) and any other fields consumed by the import-count logic so the test
matches the repository contract and exercises the new count logic.
🧹 Nitpick comments (1)
ghost/core/test/unit/server/services/verification-trigger.test.js (1)

370-383: This case doesn't actually verify “does not subscribe”.

Because DomainEvents.subscribe is restored before the trigger is created, this only proves that no source-event lookup happens after dispatch. If the constructor still subscribes and the handler exits early, the test still passes. Either assert against the subscribe stub or rename the case to the weaker behavior it really checks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghost/core/test/unit/server/services/verification-trigger.test.js` around
lines 370 - 383, The test currently restores DomainEvents.subscribe before
creating the trigger so it cannot assert whether the constructor subscribed;
update the test to either (A) keep DomainEvents.subscribe stubbed and assert it
was not called (use the existing domainEventsStub to stub/spy
DomainEvents.subscribe and then sinon.assert.notCalled(domainEventsStub) after
creating createVerificationTrigger with isVerified: true), or (B) if you intend
to test only that the handler doesn't react, rename the test to reflect that it
verifies no handler invocation (e.g., "Does not invoke handler when site is
already verified") and leave the current assertions against
eventStub/MemberCreatedEvent dispatch. Use the symbols DomainEvents.subscribe,
domainEventsStub, createVerificationTrigger, eventStub and MemberCreatedEvent to
locate and apply the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ghost/core/test/e2e-api/admin/members.test.js`:
- Around line 1018-1039: The test currently uses
receivedWebhookRequests.find(...) which will pass if one matching webhook exists
even when duplicates are sent; replace the find() with a filter: const
matchingRequests = receivedWebhookRequests.filter((request) => { ...same
predicate... }); then assert.strictEqual(matchingRequests.length, 1, 'Expected
exactly one verification webhook to be sent'); finally set const matchingRequest
= matchingRequests[0] and keep the existing timestamp/signature checks using
matchingRequest; this ensures the test fails if more than one matching webhook
is emitted.

---

Outside diff comments:
In `@ghost/core/test/integration/services/email-service/batch-sending.test.js`:
- Around line 519-556: The test leaks stubs/state because getSignupEvents is
stubbed and restoreEmailVerificationUtils() is only called on the happy path;
ensure cleanup always runs by adding a finally/afterEach cleanup that calls
sinon.restore() and restoreEmailVerificationUtils() (or calls
restoreEmailVerificationUtils() inside a try/finally around the test body) so
the sinon stub getSignupEvents and any verification helper state are always
restored even when assertions fail; locate the stub created via
sinon.stub(events, 'getSignupEvents') and the call to
restoreEmailVerificationUtils() and make their teardown unconditional.
- Around line 519-525: The stubbed getSignupEvents in the test only returns
meta.pagination.total but the verification-trigger path expects the full page
payload shape; update the sinon.stub for events.getSignupEvents to resolve with
a complete page object including properties like events (an array of event
objects matching the shape used in the code), meta.pagination (total, limit,
page, pages) and any other fields consumed by the import-count logic so the test
matches the repository contract and exercises the new count logic.

---

Nitpick comments:
In `@ghost/core/test/unit/server/services/verification-trigger.test.js`:
- Around line 370-383: The test currently restores DomainEvents.subscribe before
creating the trigger so it cannot assert whether the constructor subscribed;
update the test to either (A) keep DomainEvents.subscribe stubbed and assert it
was not called (use the existing domainEventsStub to stub/spy
DomainEvents.subscribe and then sinon.assert.notCalled(domainEventsStub) after
creating createVerificationTrigger with isVerified: true), or (B) if you intend
to test only that the handler doesn't react, rename the test to reflect that it
verifies no handler invocation (e.g., "Does not invoke handler when site is
already verified") and leave the current assertions against
eventStub/MemberCreatedEvent dispatch. Use the symbols DomainEvents.subscribe,
domainEventsStub, createVerificationTrigger, eventStub and MemberCreatedEvent to
locate and apply the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fffb7c13-edca-40e7-b6c8-2a48b0fa44c2

📥 Commits

Reviewing files that changed from the base of the PR and between abc5eee and 5b44a64.

⛔ Files ignored due to path filters (1)
  • ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap is excluded by !**/*.snap
📒 Files selected for processing (6)
  • ghost/core/core/server/services/verification-trigger.js
  • ghost/core/test/e2e-api/admin/members.test.js
  • ghost/core/test/integration/services/email-service/batch-sending.test.js
  • ghost/core/test/legacy/api/admin/members-importer.test.js
  • ghost/core/test/unit/server/services/verification-trigger.test.js
  • ghost/core/test/utils/email-verification-utils.ts
✅ Files skipped from review due to trivial changes (1)
  • ghost/core/core/server/services/verification-trigger.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • ghost/core/test/legacy/api/admin/members-importer.test.js
  • ghost/core/test/utils/email-verification-utils.ts

@aileen aileen force-pushed the gva-732-verification-flow-tests branch 2 times, most recently from 36223be to 70c4bc8 Compare April 1, 2026 07:56
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
ghost/core/test/utils/email-verification-utils.ts (2)

110-110: nock.cleanAll() is synchronous — await is unnecessary.

This won't cause issues, but cleanAll returns void, not a Promise.

♻️ Suggested fix
-    await nock.cleanAll();
+    nock.cleanAll();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghost/core/test/utils/email-verification-utils.ts` at line 110, Remove the
unnecessary await on the synchronous nock.cleanAll() call in the test helper;
locate the line calling await nock.cleanAll() and change it to a plain
nock.cleanAll() so it no longer awaits a void return from the nock.cleanAll()
function.

70-74: Extract nested ternary for readability.

The nested ternary makes this block harder to parse at a glance. Extracting to a helper or using explicit conditionals would improve clarity, though this is optional given it's test utility code.

♻️ Suggested refactor
-            const rawBody = Buffer.isBuffer(requestBody) ?
-                requestBody.toString('utf8') :
-                typeof requestBody === 'string' ?
-                    requestBody :
-                    JSON.stringify(requestBody);
+            let rawBody: string;
+            if (Buffer.isBuffer(requestBody)) {
+                rawBody = requestBody.toString('utf8');
+            } else if (typeof requestBody === 'string') {
+                rawBody = requestBody;
+            } else {
+                rawBody = JSON.stringify(requestBody);
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghost/core/test/utils/email-verification-utils.ts` around lines 70 - 74, The
nested ternary used to compute rawBody from requestBody is hard to read; replace
it with a small helper or explicit conditionals (e.g., a function like
normalizeRequestBody(requestBody) or an if/else block) that handles Buffer,
string, and other types and returns the utf8 string or JSON string; update the
usage where rawBody is assigned so the logic is clearer and unit-test behavior
is unchanged (referencing the rawBody variable and the requestBody input in
email-verification-utils.ts).
ghost/core/core/server/services/verification-trigger.js (1)

232-232: Prefer Number.isFinite over global isFinite.

The global isFinite coerces non-numeric values (e.g., isFinite("10") returns true), whereas Number.isFinite does not. Since importThreshold could hypothetically receive a string from misconfiguration, Number.isFinite is safer.

♻️ Suggested fix
-        if (isFinite(importThreshold) && currentImport > importThreshold) {
+        if (Number.isFinite(importThreshold) && currentImport > importThreshold) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghost/core/core/server/services/verification-trigger.js` at line 232, Replace
the use of the global isFinite in the conditional that checks importThreshold
(the if that currently references importThreshold and currentImport) with
Number.isFinite to avoid coercion of non-numeric values; specifically update the
conditional that reads the importThreshold check so it uses
Number.isFinite(importThreshold) while leaving the currentImport >
importThreshold comparison unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@ghost/core/core/server/services/verification-trigger.js`:
- Line 232: Replace the use of the global isFinite in the conditional that
checks importThreshold (the if that currently references importThreshold and
currentImport) with Number.isFinite to avoid coercion of non-numeric values;
specifically update the conditional that reads the importThreshold check so it
uses Number.isFinite(importThreshold) while leaving the currentImport >
importThreshold comparison unchanged.

In `@ghost/core/test/utils/email-verification-utils.ts`:
- Line 110: Remove the unnecessary await on the synchronous nock.cleanAll() call
in the test helper; locate the line calling await nock.cleanAll() and change it
to a plain nock.cleanAll() so it no longer awaits a void return from the
nock.cleanAll() function.
- Around line 70-74: The nested ternary used to compute rawBody from requestBody
is hard to read; replace it with a small helper or explicit conditionals (e.g.,
a function like normalizeRequestBody(requestBody) or an if/else block) that
handles Buffer, string, and other types and returns the utf8 string or JSON
string; update the usage where rawBody is assigned so the logic is clearer and
unit-test behavior is unchanged (referencing the rawBody variable and the
requestBody input in email-verification-utils.ts).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0b775c39-f2c0-44f3-a2b2-5ed5bfb8be1e

📥 Commits

Reviewing files that changed from the base of the PR and between 5b44a64 and 70c4bc8.

📒 Files selected for processing (4)
  • ghost/core/core/server/services/verification-trigger.js
  • ghost/core/test/e2e-api/admin/members.test.js
  • ghost/core/test/unit/server/services/verification-trigger.test.js
  • ghost/core/test/utils/email-verification-utils.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • ghost/core/test/unit/server/services/verification-trigger.test.js

@aileen aileen force-pushed the gva-732-verification-flow-tests branch from 70c4bc8 to dad9c11 Compare April 1, 2026 08:08
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ghost/core/test/utils/email-verification-utils.ts`:
- Around line 110-111: The call to nock.cleanAll() is synchronous so remove the
unnecessary await before nock.cleanAll() and leave the await on the async
configUtils.restore() call intact; update the cleanup block that currently calls
await nock.cleanAll(); await configUtils.restore(); to call nock.cleanAll();
await configUtils.restore(); so only the async restore uses await.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 78fee93b-f782-4cc9-a9a2-1f6e948aafc6

📥 Commits

Reviewing files that changed from the base of the PR and between 70c4bc8 and dad9c11.

📒 Files selected for processing (4)
  • ghost/core/core/server/services/verification-trigger.js
  • ghost/core/test/e2e-api/admin/members.test.js
  • ghost/core/test/unit/server/services/verification-trigger.test.js
  • ghost/core/test/utils/email-verification-utils.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • ghost/core/core/server/services/verification-trigger.js

@aileen aileen force-pushed the gva-732-verification-flow-tests branch from dad9c11 to 3d319fd Compare April 1, 2026 08:27
@aileen aileen changed the title Updated email verification flow tests Improved email verification trigger tests Apr 1, 2026
@aileen aileen force-pushed the gva-732-verification-flow-tests branch 3 times, most recently from 4b9cf63 to 2860908 Compare April 1, 2026 09:37
// paginated response data, which may be truncated by the default limit.
// Admin and API imports are usually only one member at a time, so this
// should be sufficient to prevent off-by-one errors without risking inaccurate
// counts from larger imports.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

// TODO: Fix off-by-one issue in event dispatch: <issue id>

Might be a good comment, we're acknowledging that there's a problem here, we should defer the extra details to an issue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

More thinking: Feels like this could be an issue with bookshelf-pagination?

We're seeing the pagination data lag behind the event data, which implies a race condition on the model. I'm not very confident, but I'll do some spelunking into the Framework repo

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yeah I think that this is the case:

2 things to look into:

  • Pagination plugin should transact to make sure the count query and data queries are based on the same data
  • If the data query returns less than the page limit then we should build the pagination data from the data query (i.e. meta.pagination.total = data.length)

Copy link
Copy Markdown
Contributor

@sam-lord sam-lord left a comment

Choose a reason for hiding this comment

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

Woo hoo!!! Lets get it merged

// paginated response data, which may be truncated by the default limit.
// Admin and API imports are usually only one member at a time, so this
// should be sufficient to prevent off-by-one errors without risking inaccurate
// counts from larger imports.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

More thinking: Feels like this could be an issue with bookshelf-pagination?

We're seeing the pagination data lag behind the event data, which implies a race condition on the model. I'm not very confident, but I'll do some spelunking into the Framework repo

// paginated response data, which may be truncated by the default limit.
// Admin and API imports are usually only one member at a time, so this
// should be sufficient to prevent off-by-one errors without risking inaccurate
// counts from larger imports.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yeah I think that this is the case:

2 things to look into:

  • Pagination plugin should transact to make sure the count query and data queries are based on the same data
  • If the data query returns less than the page limit then we should build the pagination data from the data query (i.e. meta.pagination.total = data.length)

* @param {number} [options.newsletterCount] - The number of newsletters to expect in the response
* @returns {Promise<object>} The created member
*/
const createMemberThroughApi = async (options) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Love this helper

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This file 🤩

ref [#27013](#27013)

This consolidates the email verification test updates ahead of the `verificationFlow` GA rollout, where the legacy email path will be removed in favor of webhook-based verification. It refactors the admin and unit tests around the new helpers, hardens `email-verification-utils.ts`, tightens the webhook assertions, and updates `VerificationTrigger` counting so `api` and `admin` signup events behave correctly once the new flow becomes the default.
@aileen aileen force-pushed the gva-732-verification-flow-tests branch from 2860908 to c70ece4 Compare April 1, 2026 12:08
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 1, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
7.3% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@aileen aileen enabled auto-merge (squash) April 1, 2026 12:19
@aileen aileen merged commit f6dfb4d into main Apr 1, 2026
36 of 37 checks passed
@aileen aileen deleted the gva-732-verification-flow-tests branch April 1, 2026 12:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants