Skip to content

🐛 Fixed private mentions being treated as public#1429

Merged
sagzy merged 7 commits intomainfrom
bug/private-mentions
Nov 13, 2025
Merged

🐛 Fixed private mentions being treated as public#1429
sagzy merged 7 commits intomainfrom
bug/private-mentions

Conversation

@sagzy
Copy link
Contributor

@sagzy sagzy commented Nov 13, 2025

ref https://linear.app/ghost/issue/BER-2987

  • we don't currently support private mentions or DMs and were wrongly treating them as public content
  • now added an extra check to ignore non-public content when handling incoming Create activities

@coderabbitai
Copy link

coderabbitai bot commented Nov 13, 2025

Walkthrough

Adds a PUBLIC_COLLECTION import and a pre-processing step to the Create activity handler that computes recipients from to/cc, detects whether the activity is public by checking for PUBLIC_COLLECTION.href, and exits early with a log when the activity is not public. Also adds a unit test suite exercising CreateHandler for missing id, missing object.id, non-public activities, and a valid public Create that calls PostService.getByApId and persists the Create JSON to the global DB.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

  • Changes localized to two files: src/activity-handlers/create.handler.ts and src/activity-handlers/create.handler.unit.test.ts
  • Mix of a small logic addition (recipient/public check) and multiple unit tests following consistent mocking/assertion patterns

Areas to pay attention to:

  • Correctness of recipient extraction and comparison against PUBLIC_COLLECTION.href
  • Placement of the early exit before any post retrieval/storage
  • Unit tests' coverage of edge cases (missing ids, non-public, successful persist)

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main fix: preventing private mentions from being treated as public, which matches the changeset's core logic of checking if content is public before processing.
Description check ✅ Passed The description accurately explains the bug fix and the solution implemented: adding a check to ignore non-public content in Create activity handlers, which directly corresponds to the code changes.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bug/private-mentions

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 50d0f4c and 11e153d.

📒 Files selected for processing (1)
  • src/activity-handlers/create.handler.unit.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/activity-handlers/create.handler.unit.test.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build / Build Docker Images (linux/amd64, amd64)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link

@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

🧹 Nitpick comments (2)
features/support/notifications.js (1)

42-42: Inconsistent error messages in retry handlers.

The error message at line 42 omits the retry count, while the similar error at line 79 includes (${MAX_RETRIES}) and additional context. Consider keeping the messages consistent for better debugging, or document why this difference is intentional.

-            `Max retries reached when waiting on item in notifications`,
+            `Max retries reached (${MAX_RETRIES}) when waiting on item in notifications`,

Note: If the simplified message is required for test assertions (as seen in mention_steps.js line 124), consider using a more flexible matching approach in tests rather than exact string comparison.

src/activity-handlers/create.handler.ts (1)

24-32: Approve the public collection check with a defensive coding suggestion.

The logic correctly filters out non-public activities by checking for PUBLIC_COLLECTION.href in the recipients. This aligns with the ActivityPub specification and addresses the PR's objective.

Consider adding defensive checks in case toIds or ccIds are undefined:

-        const recipients = [...create.toIds, ...create.ccIds].map(
+        const recipients = [...(create.toIds ?? []), ...(create.ccIds ?? [])].map(
             (id) => id.href,
         );

This prevents potential runtime errors if Fedify's Create object has undefined arrays in edge cases.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b4f95ca and 8e83f76.

📒 Files selected for processing (5)
  • features/handle-mention.feature (1 hunks)
  • features/step_definitions/activitypub_steps.js (0 hunks)
  • features/step_definitions/mention_steps.js (1 hunks)
  • features/support/notifications.js (1 hunks)
  • src/activity-handlers/create.handler.ts (2 hunks)
💤 Files with no reviewable changes (1)
  • features/step_definitions/activitypub_steps.js
🧰 Additional context used
📓 Path-based instructions (4)
features/**/*.feature

📄 CodeRabbit inference engine (AGENTS.md)

e2e tests should reside in the features directory

Files:

  • features/handle-mention.feature
src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.ts: Never use direct string comparisons for ActivityPub IDs; use SHA256 hash-based lookups instead
Use Result helper functions (isError, getError, getValue) and avoid destructuring Result values directly
Awilix uses CLASSIC injection: constructor parameter names must match their registration names
Define routes using decorators rather than direct registration
Prefer immutable entities that emit domain events over mutable entities with dirty flags
Use structured error objects with context in Result types instead of string literals
Prefer class-based handlers with dependency injection over function factories

Files:

  • src/activity-handlers/create.handler.ts
src/activity-handlers/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Implement new handlers using the class-based pattern under /activity-handlers

Files:

  • src/activity-handlers/create.handler.ts
features/step_definitions/**/*_steps.js

📄 CodeRabbit inference engine (AGENTS.md)

Group Cucumber step definitions by high-level feature and name files with the _steps.js suffix under features/step_definitions

Files:

  • features/step_definitions/mention_steps.js
🧠 Learnings (3)
📓 Common learnings
Learnt from: allouis
Repo: TryGhost/ActivityPub PR: 1010
File: src/app.ts:744-744
Timestamp: 2025-07-09T06:47:50.064Z
Learning: In the TryGhost/ActivityPub repository, allouis prefers to keep refactoring PRs focused on code restructuring only, without adding new functionality or changing behavior. When moving existing code that has issues, the issues should be preserved in the refactoring and addressed separately.
Learnt from: allouis
Repo: TryGhost/ActivityPub PR: 737
File: migrate/migrations/000054_populate-outboxes-table.up.sql:56-63
Timestamp: 2025-05-27T09:09:22.110Z
Learning: In the outboxes table migration for Ghost ActivityPub, the author_id field always refers to the author of the original post, not the person performing the outbox action. For reposts (outbox_type = 1), author_id should be posts.author_id (original author), while user_id captures who made the repost via the joins through accounts and users tables.
📚 Learning: 2025-09-22T13:57:08.390Z
Learnt from: CR
Repo: TryGhost/ActivityPub PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T13:57:08.390Z
Learning: Applies to src/activity-handlers/**/*.ts : Implement new handlers using the class-based pattern under /activity-handlers

Applied to files:

  • src/activity-handlers/create.handler.ts
📚 Learning: 2025-09-22T13:57:08.390Z
Learnt from: CR
Repo: TryGhost/ActivityPub PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T13:57:08.390Z
Learning: Applies to features/step_definitions/**/*_steps.js : Group Cucumber step definitions by high-level feature and name files with the _steps.js suffix under features/step_definitions

Applied to files:

  • features/step_definitions/mention_steps.js
🧬 Code graph analysis (1)
features/step_definitions/mention_steps.js (3)
features/support/fixtures.js (2)
  • createObject (64-105)
  • createActivity (107-242)
features/support/request.js (1)
  • fetchActivityPub (9-36)
features/support/notifications.js (3)
  • found (32-34)
  • found (71-71)
  • waitForItemInNotifications (3-54)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build / Build Docker Images (linux/amd64, amd64)
🔇 Additional comments (3)
features/handle-mention.feature (1)

1-11: LGTM! Clear test coverage for public vs private mentions.

The feature file now explicitly distinguishes between public and private mentions with appropriate assertions. The new private mention scenario correctly validates that private mentions do not generate notifications, aligning with the PR's objective to fix private mentions being treated as public.

features/step_definitions/mention_steps.js (2)

9-47: LGTM! Public mention step is well-structured.

The step correctly:

  • Validates actor existence
  • Creates a public Note with Mention tags
  • Uses default public delivery (to='as:Public')
  • Stores the mention ID and objects for later assertions

Good error handling for missing actors.


49-94: LGTM! Private mention step correctly implements private delivery.

The step properly creates a private mention by:

  • Setting object.to and activity.to to only the local actor ID
  • Setting cc to empty array
  • This ensures the mention is not public and won't be processed by the handler's public check

Copy link

@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: 0

♻️ Duplicate comments (1)
features/step_definitions/mention_steps.js (1)

96-105: Fix the notification field matcher to check the correct field for ActivityPub URLs.

The helper waitForItemInNotifications checks notification.post?.id when given a string input, but post.id is a numeric database identifier. Since this.mentionId is an ActivityPub URL (object.id), the comparison will always fail. Update the matcher to check notification.post?.url instead.

🧹 Nitpick comments (1)
features/step_definitions/mention_steps.js (1)

107-123: Consider a dedicated helper for negative assertions to improve test performance.

The current pattern verifies absence by catching the retry timeout error after ~5 seconds. While functional, this approach is slow and couples the test to the error message string. Consider adding a dedicated expectNotInNotifications helper that returns a boolean after a shorter timeout, making negative assertions faster and less fragile.

Example helper in features/support/notifications.js:

export async function expectNotInNotifications(input, maxWaitMs = 2000) {
    const startTime = Date.now();
    while (Date.now() - startTime < maxWaitMs) {
        const response = await fetchActivityPub(
            'https://self.test/.ghost/activitypub/v1/notifications',
            { headers: { Accept: 'application/ld+json' } }
        );
        const json = await response.json();
        const matcher = typeof input === 'string' 
            ? (n) => n.post?.id === input 
            : input;
        if (json.notifications.find(matcher)) {
            return false;
        }
        await new Promise((resolve) => setTimeout(resolve, 400));
    }
    return true;
}

Then update the step:

 Then('the mention is not in our notifications', async function () {
     if (!this.mentionId) {
         throw new Error(
             'You need to call a step which creates a mention before this',
         );
     }
 
-    try {
-        await waitForItemInNotifications(this.mentionId);
-        assert.fail(`Expected mention ${this.mentionId} to not be in notifications`);
-    } catch (error) {
-        assert.equal(
-            error.message,
-            `Max retries reached when waiting on item in notifications`,
-        );
-    }
+    const notFound = await expectNotInNotifications(this.mentionId);
+    assert(notFound, `Expected mention ${this.mentionId} to not be in notifications`);
 });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e83f76 and 0cd7aec.

📒 Files selected for processing (5)
  • features/handle-mention.feature (1 hunks)
  • features/step_definitions/activitypub_steps.js (0 hunks)
  • features/step_definitions/mention_steps.js (1 hunks)
  • features/support/notifications.js (1 hunks)
  • src/activity-handlers/create.handler.ts (2 hunks)
💤 Files with no reviewable changes (1)
  • features/step_definitions/activitypub_steps.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • features/handle-mention.feature
  • features/support/notifications.js
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.ts: Never use direct string comparisons for ActivityPub IDs; use SHA256 hash-based lookups instead
Use Result helper functions (isError, getError, getValue) and avoid destructuring Result values directly
Awilix uses CLASSIC injection: constructor parameter names must match their registration names
Define routes using decorators rather than direct registration
Prefer immutable entities that emit domain events over mutable entities with dirty flags
Use structured error objects with context in Result types instead of string literals
Prefer class-based handlers with dependency injection over function factories

Files:

  • src/activity-handlers/create.handler.ts
src/activity-handlers/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Implement new handlers using the class-based pattern under /activity-handlers

Files:

  • src/activity-handlers/create.handler.ts
features/step_definitions/**/*_steps.js

📄 CodeRabbit inference engine (AGENTS.md)

Group Cucumber step definitions by high-level feature and name files with the _steps.js suffix under features/step_definitions

Files:

  • features/step_definitions/mention_steps.js
🧠 Learnings (2)
📓 Common learnings
Learnt from: allouis
Repo: TryGhost/ActivityPub PR: 1010
File: src/app.ts:744-744
Timestamp: 2025-07-09T06:47:50.064Z
Learning: In the TryGhost/ActivityPub repository, allouis prefers to keep refactoring PRs focused on code restructuring only, without adding new functionality or changing behavior. When moving existing code that has issues, the issues should be preserved in the refactoring and addressed separately.
Learnt from: allouis
Repo: TryGhost/ActivityPub PR: 737
File: migrate/migrations/000054_populate-outboxes-table.up.sql:56-63
Timestamp: 2025-05-27T09:09:22.110Z
Learning: In the outboxes table migration for Ghost ActivityPub, the author_id field always refers to the author of the original post, not the person performing the outbox action. For reposts (outbox_type = 1), author_id should be posts.author_id (original author), while user_id captures who made the repost via the joins through accounts and users tables.
Learnt from: mike182uk
Repo: TryGhost/ActivityPub PR: 606
File: src/activitypub/fediverse-bridge.unit.test.ts:158-167
Timestamp: 2025-05-01T13:08:36.516Z
Learning: In the ActivityPub FediverseBridge test suite, expect assertions on sendActivity calls should verify the `username` property, not `handle`, as username is the correct parameter name.
📚 Learning: 2025-09-22T13:57:08.390Z
Learnt from: CR
Repo: TryGhost/ActivityPub PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T13:57:08.390Z
Learning: Applies to src/activity-handlers/**/*.ts : Implement new handlers using the class-based pattern under /activity-handlers

Applied to files:

  • src/activity-handlers/create.handler.ts
🧬 Code graph analysis (1)
features/step_definitions/mention_steps.js (3)
features/support/fixtures.js (2)
  • createObject (64-105)
  • createActivity (107-242)
features/support/request.js (1)
  • fetchActivityPub (9-36)
features/support/notifications.js (3)
  • found (32-34)
  • found (71-71)
  • waitForItemInNotifications (3-54)
🔇 Additional comments (3)
src/activity-handlers/create.handler.ts (1)

1-1: LGTM! Public content filtering correctly implemented.

The addition of PUBLIC_COLLECTION-based filtering correctly prevents private mentions and DMs from being processed as public content. The logic appropriately checks both to and cc recipients and exits early before any post storage occurs.

Also applies to: 24-32

features/step_definitions/mention_steps.js (2)

9-47: LGTM! Public mention step correctly structured.

The step definition properly constructs a public mention using the existing fixture helpers, stores the necessary context for assertions, and posts to the inbox. The default public addressing aligns with the test scenario.


49-94: LGTM! Private mention step correctly implements private addressing.

The step properly overrides both object and activity recipients to create a private mention. Setting to to the local actor and clearing cc ensures the Create activity will not include PUBLIC_COLLECTION and will be filtered by the handler.

ref https://linear.app/ghost/issue/BER-2987

- we currently don't support private mentions or DMs, and they were wrongly been treated as public content
- added an extra check when handling Create activity, to ignore non-public content
@sagzy sagzy force-pushed the bug/private-mentions branch from 0cd7aec to 0629e08 Compare November 13, 2025 09:42
Copy link

@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: 0

🧹 Nitpick comments (2)
features/step_definitions/mention_steps.js (2)

32-33: Consider explicit public addressing for clarity.

The step relies on createObject defaults to set to='as:Public'. While documented in the comment, making this explicit would improve robustness and clarity.

Consider passing public addressing explicitly:

-    // Create a public Note with the mention (uses defaults: to='as:Public')
-    const object = await createObject('Note', actor, `Hello ${mention}`, tags);
+    // Create a public Note with the mention
+    const object = await createObject('Note', actor, `Hello ${mention}`, tags);
+    object.to = 'as:Public';

Alternatively, if createObject supports it, pass to as a parameter to avoid mutation.


72-81: Post-creation mutation works but consider cleaner approach.

The private mention implementation mutates to and cc properties after object/activity creation. While functional, this pattern could be fragile if the creation functions add additional addressing logic in the future.

If createObject and createActivity support custom addressing parameters, consider passing them during creation rather than mutating afterward. If not supported, the current approach is acceptable.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0cd7aec and 0629e08.

📒 Files selected for processing (5)
  • features/handle-mention.feature (1 hunks)
  • features/step_definitions/activitypub_steps.js (0 hunks)
  • features/step_definitions/mention_steps.js (1 hunks)
  • features/support/notifications.js (1 hunks)
  • src/activity-handlers/create.handler.ts (2 hunks)
💤 Files with no reviewable changes (1)
  • features/step_definitions/activitypub_steps.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • features/handle-mention.feature
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.ts: Never use direct string comparisons for ActivityPub IDs; use SHA256 hash-based lookups instead
Use Result helper functions (isError, getError, getValue) and avoid destructuring Result values directly
Awilix uses CLASSIC injection: constructor parameter names must match their registration names
Define routes using decorators rather than direct registration
Prefer immutable entities that emit domain events over mutable entities with dirty flags
Use structured error objects with context in Result types instead of string literals
Prefer class-based handlers with dependency injection over function factories

Files:

  • src/activity-handlers/create.handler.ts
src/activity-handlers/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Implement new handlers using the class-based pattern under /activity-handlers

Files:

  • src/activity-handlers/create.handler.ts
features/step_definitions/**/*_steps.js

📄 CodeRabbit inference engine (AGENTS.md)

Group Cucumber step definitions by high-level feature and name files with the _steps.js suffix under features/step_definitions

Files:

  • features/step_definitions/mention_steps.js
🧠 Learnings (5)
📓 Common learnings
Learnt from: allouis
Repo: TryGhost/ActivityPub PR: 1010
File: src/app.ts:744-744
Timestamp: 2025-07-09T06:47:50.064Z
Learning: In the TryGhost/ActivityPub repository, allouis prefers to keep refactoring PRs focused on code restructuring only, without adding new functionality or changing behavior. When moving existing code that has issues, the issues should be preserved in the refactoring and addressed separately.
Learnt from: allouis
Repo: TryGhost/ActivityPub PR: 1021
File: src/post/post.entity.ts:327-329
Timestamp: 2025-07-10T08:39:14.900Z
Learning: In the TryGhost/ActivityPub repository, when parsing data from Ghost CMS (such as post excerpts) using branded types with validation (like PostSummary.parse()), allouis prefers to let the parsing throw errors if Ghost sends incompatible data rather than adding defensive handling like truncation. This approach helps detect incompatibilities between the ActivityPub system and Ghost CMS early, ensuring issues are addressed at the source rather than silently ignored.
Learnt from: mike182uk
Repo: TryGhost/ActivityPub PR: 606
File: src/activitypub/fediverse-bridge.unit.test.ts:158-167
Timestamp: 2025-05-01T13:08:36.516Z
Learning: In the ActivityPub FediverseBridge test suite, expect assertions on sendActivity calls should verify the `username` property, not `handle`, as username is the correct parameter name.
📚 Learning: 2025-09-22T13:57:08.390Z
Learnt from: CR
Repo: TryGhost/ActivityPub PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T13:57:08.390Z
Learning: Applies to src/activity-handlers/**/*.ts : Implement new handlers using the class-based pattern under /activity-handlers

Applied to files:

  • src/activity-handlers/create.handler.ts
📚 Learning: 2025-09-22T13:57:08.390Z
Learnt from: CR
Repo: TryGhost/ActivityPub PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T13:57:08.390Z
Learning: Applies to features/step_definitions/**/*_steps.js : Group Cucumber step definitions by high-level feature and name files with the _steps.js suffix under features/step_definitions

Applied to files:

  • features/step_definitions/mention_steps.js
📚 Learning: 2025-07-09T06:47:50.064Z
Learnt from: allouis
Repo: TryGhost/ActivityPub PR: 1010
File: src/app.ts:744-744
Timestamp: 2025-07-09T06:47:50.064Z
Learning: In the TryGhost/ActivityPub repository, allouis prefers to keep refactoring PRs focused on code restructuring only, without adding new functionality or changing behavior. When moving existing code that has issues, the issues should be preserved in the refactoring and addressed separately.

Applied to files:

  • features/step_definitions/mention_steps.js
📚 Learning: 2025-05-27T09:09:22.110Z
Learnt from: allouis
Repo: TryGhost/ActivityPub PR: 737
File: migrate/migrations/000054_populate-outboxes-table.up.sql:56-63
Timestamp: 2025-05-27T09:09:22.110Z
Learning: In the outboxes table migration for Ghost ActivityPub, the author_id field always refers to the author of the original post, not the person performing the outbox action. For reposts (outbox_type = 1), author_id should be posts.author_id (original author), while user_id captures who made the repost via the joins through accounts and users tables.

Applied to files:

  • features/step_definitions/mention_steps.js
🧬 Code graph analysis (1)
features/step_definitions/mention_steps.js (3)
features/support/fixtures.js (2)
  • createObject (64-105)
  • createActivity (107-242)
features/support/request.js (1)
  • fetchActivityPub (9-36)
features/support/notifications.js (3)
  • found (32-34)
  • found (71-71)
  • waitForItemInNotifications (3-54)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build / Build Docker Images (linux/amd64, amd64)
🔇 Additional comments (5)
features/support/notifications.js (1)

42-42: Error message simplified for test assertions.

The dynamic MAX_RETRIES value was removed from the error message to enable exact string matching in tests (see mention_steps.js line 122).

src/activity-handlers/create.handler.ts (2)

1-1: LGTM!

Correct import of PUBLIC_COLLECTION for public/private content detection.


24-32: Code is correct—verification confirms spread operator safety.

Fedify's toIds and ccIds are non-functional (multi) properties that hold arrays of recipient IRIs, eliminating any TypeError risk from the spread operator at lines 24–26. The public/private detection logic correctly addresses the PR objective by filtering non-public content before post retrieval.

features/step_definitions/mention_steps.js (2)

96-105: LGTM!

Clean positive assertion with proper error handling for missing context.


107-125: Negative assertion correctly implemented.

The retry-based negative check is consistent with the positive assertion (both check notification.post?.id) and properly validates the error message from waitForItemInNotifications.

Comment on lines +107 to +125
Then('the mention is not in our notifications', async function () {
if (!this.mentionId) {
throw new Error(
'You need to call a step which creates a mention before this',
);
}

try {
await waitForItemInNotifications(this.mentionId);
assert.fail(
`Expected mention ${this.mentionId} to not be in notifications`,
);
} catch (error) {
assert.equal(
error.message,
`Max retries reached when waiting on item in notifications`,
);
}
});
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure about this - this step is always going to take ~7.5s to run (waiting for the max timeout)

I get why we need to do this, but it doesn't feel great. Would we be better just keeping the happy path at the the e2e level, and then testing the non-happy paths at the unit (or integration) level?

Also, semantically, not receiving a private mention is not really a feature, more an implementation detail (because we don't support private mention) so we probably don't need to document it via cucumber?

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah that's fair! I wanted to have a e2e test here to reproduce the actual bug that was reported - a private mention that is treated as public content - but agree the negative e2e test is slow.

Let me see if I move this test to unit / integratio

@TryGhost TryGhost deleted a comment from coderabbitai bot Nov 13, 2025
Copy link

@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: 0

🧹 Nitpick comments (2)
src/activity-handlers/create.handler.unit.test.ts (2)

94-110: Add test coverage for PUBLIC_COLLECTION in ccIds.

The test correctly verifies early exit when PUBLIC_COLLECTION is not in toIds. However, the handler checks both toIds and ccIds when determining if a Create activity is public. Consider adding a complementary test to verify that the handler accepts a Create activity when PUBLIC_COLLECTION appears in ccIds.

Add this test case:

it('should process Create activity when PUBLIC_COLLECTION is in ccIds', async () => {
    const mockAccount = await createTestExternalAccount(1, {
        username: 'testuser',
        name: 'Test User',
        bio: null,
        url: null,
        avatarUrl: null,
        bannerImageUrl: null,
        customFields: null,
        apId: new URL('https://example.com/users/testuser'),
        apFollowers: null,
        apInbox: new URL('https://example.com/users/testuser/inbox'),
    });

    const postApId = new URL('https://example.com/post/123');
    const mockPost = new Post(
        1,
        'post-uuid',
        mockAccount,
        PostType.Article,
        Audience.Public,
        PostTitle.parse('Test Post'),
        PostSummary.parse('Test excerpt'),
        null,
        'Test content',
        postApId,
        null,
        new Date(),
        { ghostAuthors: [] },
        0,
        0,
        0,
        null,
        null,
        null,
        [],
        postApId,
    );

    vi.mocked(mockPostService.getByApId).mockResolvedValue(ok(mockPost));

    const mockCreateJson = {
        '@context': 'https://www.w3.org/ns/activitystreams',
        type: 'Create',
        id: 'https://example.com/create/123',
    };

    const mockCreate = {
        id: new URL('https://example.com/create/123'),
        objectId: new URL('https://example.com/post/123'),
        toIds: [new URL('https://example.com/users/specific-user')],
        ccIds: [PUBLIC_COLLECTION], // PUBLIC_COLLECTION in cc instead of to
        toJsonLd: vi.fn().mockResolvedValue(mockCreateJson),
    } as unknown as Create;

    await handler.handle(mockContext, mockCreate);

    expect(mockPostService.getByApId).toHaveBeenCalled();
    expect(mockGlobalDb.set).toHaveBeenCalled();
});

112-179: Consider adding tests for PostService error paths.

The handler has specific error handling for upstream-error, not-a-post, and missing-author cases from PostService.getByApId. Adding tests for these error paths would improve coverage and ensure the handler logs appropriately without crashing or storing incomplete data.

Example test for error case:

it('should exit gracefully when PostService returns upstream-error', async () => {
    const mockCreate = {
        id: new URL('https://example.com/create/123'),
        objectId: new URL('https://example.com/post/123'),
        toIds: [PUBLIC_COLLECTION],
        ccIds: [],
        toJsonLd: vi.fn(),
    } as unknown as Create;

    vi.mocked(mockPostService.getByApId).mockResolvedValue(
        error('upstream-error'),
    );

    await handler.handle(mockContext, mockCreate);

    expect(mockLogger.info).toHaveBeenCalledWith(
        'Upstream error fetching post for create handling',
        expect.any(Object),
    );
    expect(mockGlobalDb.set).not.toHaveBeenCalled();
});
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 47a23dd and 1ee4332.

📒 Files selected for processing (3)
  • features/handle-mention.feature (1 hunks)
  • features/step_definitions/mention_steps.js (1 hunks)
  • src/activity-handlers/create.handler.unit.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • features/step_definitions/mention_steps.js
  • features/handle-mention.feature
🧰 Additional context used
📓 Path-based instructions (4)
**/*.unit.test.ts

📄 CodeRabbit inference engine (AGENTS.md)

Use the .unit.test.ts file extension for unit tests

Files:

  • src/activity-handlers/create.handler.unit.test.ts
**/*.test.ts

📄 CodeRabbit inference engine (AGENTS.md)

All unit and integration test files should use the .test.ts suffix

Files:

  • src/activity-handlers/create.handler.unit.test.ts
src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.ts: Never use direct string comparisons for ActivityPub IDs; use SHA256 hash-based lookups instead
Use Result helper functions (isError, getError, getValue) and avoid destructuring Result values directly
Awilix uses CLASSIC injection: constructor parameter names must match their registration names
Define routes using decorators rather than direct registration
Prefer immutable entities that emit domain events over mutable entities with dirty flags
Use structured error objects with context in Result types instead of string literals
Prefer class-based handlers with dependency injection over function factories

Files:

  • src/activity-handlers/create.handler.unit.test.ts
src/activity-handlers/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Implement new handlers using the class-based pattern under /activity-handlers

Files:

  • src/activity-handlers/create.handler.unit.test.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: allouis
Repo: TryGhost/ActivityPub PR: 1010
File: src/app.ts:744-744
Timestamp: 2025-07-09T06:47:50.064Z
Learning: In the TryGhost/ActivityPub repository, allouis prefers to keep refactoring PRs focused on code restructuring only, without adding new functionality or changing behavior. When moving existing code that has issues, the issues should be preserved in the refactoring and addressed separately.
Learnt from: allouis
Repo: TryGhost/ActivityPub PR: 737
File: migrate/migrations/000054_populate-outboxes-table.up.sql:56-63
Timestamp: 2025-05-27T09:09:22.110Z
Learning: In the outboxes table migration for Ghost ActivityPub, the author_id field always refers to the author of the original post, not the person performing the outbox action. For reposts (outbox_type = 1), author_id should be posts.author_id (original author), while user_id captures who made the repost via the joins through accounts and users tables.
Learnt from: CR
Repo: TryGhost/ActivityPub PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T13:57:08.390Z
Learning: Applies to src/activity-handlers/**/*.ts : Implement new handlers using the class-based pattern under /activity-handlers
📚 Learning: 2025-09-22T13:57:08.390Z
Learnt from: CR
Repo: TryGhost/ActivityPub PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T13:57:08.390Z
Learning: Applies to src/activity-handlers/**/*.ts : Implement new handlers using the class-based pattern under /activity-handlers

Applied to files:

  • src/activity-handlers/create.handler.unit.test.ts
🧬 Code graph analysis (1)
src/activity-handlers/create.handler.unit.test.ts (5)
src/activity-handlers/create.handler.ts (1)
  • CreateHandler (7-72)
src/post/post.service.ts (1)
  • PostService (68-550)
src/app.ts (1)
  • ContextData (169-172)
src/test/account-entity-test-helpers.ts (1)
  • createTestExternalAccount (92-118)
src/core/result.ts (1)
  • ok (44-46)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build / Build Docker Images (linux/amd64, amd64)
🔇 Additional comments (4)
src/activity-handlers/create.handler.unit.test.ts (4)

1-16: LGTM!

Imports are clean and appropriate for the test suite. All necessary dependencies are included.


18-55: LGTM!

Test setup is clean with proper mock initialization in beforeEach. The mock structure correctly implements the expected interfaces.


58-92: LGTM!

Early exit tests for missing id and objectId are well-structured and correctly verify that no downstream operations occur.


112-179: ****

The original review comment incorrectly identified the issue. The Post constructor has two distinct URL fields: url (parameter 10) and apId (parameter 21). The test correctly passes postApId to both fields because the post's URL is its ActivityPub ID—this is intentional design, not a copy-paste error. Parameter 9 is content (a string) and parameter 18 is threadRoot (a number), neither of which is duplicated or problematic.

Likely an incorrect or invalid review comment.

Copy link

@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: 0

🧹 Nitpick comments (2)
src/activity-handlers/create.handler.unit.test.ts (2)

94-110: LGTM! This test validates the core PR objective.

This test correctly verifies that private/unlisted Create activities (not addressed to PUBLIC_COLLECTION) are ignored, directly addressing the bug fix for private mentions being treated as public.

Consider adding a test case where PUBLIC_COLLECTION appears in ccIds rather than toIds to verify the handler correctly checks both arrays:

it('should process Create activity when PUBLIC_COLLECTION is in ccIds', async () => {
    // Setup similar to valid test but with:
    // toIds: [new URL('https://example.com/users/specific-user')],
    // ccIds: [PUBLIC_COLLECTION],
    // Should still be treated as public
});

112-179: LGTM! Comprehensive test for the success path.

This test thoroughly validates that a valid public Create activity is processed correctly, including post retrieval and storage.

Consider adding test cases for error paths from postService.getByApId to ensure the handler gracefully handles failures:

it('should handle upstream-error from postService', async () => {
    vi.mocked(mockPostService.getByApId).mockResolvedValue(
        error('upstream-error')
    );
    
    const mockCreate = {
        id: new URL('https://example.com/create/123'),
        objectId: new URL('https://example.com/post/123'),
        toIds: [PUBLIC_COLLECTION],
        ccIds: [],
        toJsonLd: vi.fn(),
    } as unknown as Create;

    await handler.handle(mockContext, mockCreate);

    expect(mockLogger.info).toHaveBeenCalledWith(
        'Upstream error fetching post for create handling',
        expect.any(Object)
    );
    expect(mockGlobalDb.set).not.toHaveBeenCalled();
});

// Similar tests for 'not-a-post' and 'missing-author' errors

Note: You'll need to import error from @/core/result alongside the existing ok import.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1ee4332 and 50d0f4c.

📒 Files selected for processing (1)
  • src/activity-handlers/create.handler.unit.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.unit.test.ts

📄 CodeRabbit inference engine (AGENTS.md)

Use the .unit.test.ts file extension for unit tests

Files:

  • src/activity-handlers/create.handler.unit.test.ts
**/*.test.ts

📄 CodeRabbit inference engine (AGENTS.md)

All unit and integration test files should use the .test.ts suffix

Files:

  • src/activity-handlers/create.handler.unit.test.ts
src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.ts: Never use direct string comparisons for ActivityPub IDs; use SHA256 hash-based lookups instead
Use Result helper functions (isError, getError, getValue) and avoid destructuring Result values directly
Awilix uses CLASSIC injection: constructor parameter names must match their registration names
Define routes using decorators rather than direct registration
Prefer immutable entities that emit domain events over mutable entities with dirty flags
Use structured error objects with context in Result types instead of string literals
Prefer class-based handlers with dependency injection over function factories

Files:

  • src/activity-handlers/create.handler.unit.test.ts
src/activity-handlers/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Implement new handlers using the class-based pattern under /activity-handlers

Files:

  • src/activity-handlers/create.handler.unit.test.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: allouis
Repo: TryGhost/ActivityPub PR: 1010
File: src/app.ts:744-744
Timestamp: 2025-07-09T06:47:50.064Z
Learning: In the TryGhost/ActivityPub repository, allouis prefers to keep refactoring PRs focused on code restructuring only, without adding new functionality or changing behavior. When moving existing code that has issues, the issues should be preserved in the refactoring and addressed separately.
Learnt from: allouis
Repo: TryGhost/ActivityPub PR: 737
File: migrate/migrations/000054_populate-outboxes-table.up.sql:56-63
Timestamp: 2025-05-27T09:09:22.110Z
Learning: In the outboxes table migration for Ghost ActivityPub, the author_id field always refers to the author of the original post, not the person performing the outbox action. For reposts (outbox_type = 1), author_id should be posts.author_id (original author), while user_id captures who made the repost via the joins through accounts and users tables.
Learnt from: mike182uk
Repo: TryGhost/ActivityPub PR: 606
File: src/activitypub/fediverse-bridge.unit.test.ts:158-167
Timestamp: 2025-05-01T13:08:36.516Z
Learning: In the ActivityPub FediverseBridge test suite, expect assertions on sendActivity calls should verify the `username` property, not `handle`, as username is the correct parameter name.
📚 Learning: 2025-09-22T13:57:08.390Z
Learnt from: CR
Repo: TryGhost/ActivityPub PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-22T13:57:08.390Z
Learning: Applies to src/activity-handlers/**/*.ts : Implement new handlers using the class-based pattern under /activity-handlers

Applied to files:

  • src/activity-handlers/create.handler.unit.test.ts
🧬 Code graph analysis (1)
src/activity-handlers/create.handler.unit.test.ts (6)
src/activity-handlers/create.handler.ts (1)
  • CreateHandler (7-72)
src/post/post.service.ts (1)
  • PostService (68-550)
src/app.ts (1)
  • ContextData (169-172)
src/test/account-entity-test-helpers.ts (1)
  • createTestExternalAccount (92-118)
src/post/post.entity.ts (1)
  • Post (143-687)
src/core/result.ts (1)
  • ok (44-46)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build / Build Docker Images (linux/amd64, amd64)
🔇 Additional comments (3)
src/activity-handlers/create.handler.unit.test.ts (3)

1-55: LGTM! Well-structured test setup.

The test setup is clean with appropriate mocks for all dependencies. The beforeEach hook properly initializes fresh mocks for each test case, preventing test interdependencies.


58-74: LGTM! Correctly validates the missing id guard.

This test properly verifies that the handler exits early when the Create activity lacks an id, preventing any downstream operations.


76-92: LGTM! Correctly validates the missing objectId guard.

This test properly verifies that the handler exits early when the Create activity lacks an objectId, which is essential for preventing null pointer issues in downstream processing.

@sagzy sagzy enabled auto-merge (squash) November 13, 2025 15:54
@sagzy sagzy merged commit b884710 into main Nov 13, 2025
11 checks passed
@sagzy sagzy deleted the bug/private-mentions branch November 13, 2025 16:09
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