Skip to content

Conversation

@mike182uk
Copy link
Member

no refs

Refactored cucumber steps into separate files, grouped by either feature or functionality (for generic steps). stepdefs.js has been renamed to index.js and contains the hooks to execute as well as any global setup

@coderabbitai
Copy link

coderabbitai bot commented May 1, 2025

Walkthrough

This change removes the large, monolithic features/step_definitions/stepdefs.js file, which previously contained a comprehensive suite of Cucumber step definitions and helper functions for ActivityPub integration testing. In its place, the change introduces a modular structure by adding multiple new step definition files, each focused on a specific area of ActivityPub or application functionality. These new files include step definitions for account details, ActivityPub protocol operations, feeds, follows, likes, notes, posts, replies, reposts, requests, site endpoints, threads, and webhooks. Supporting utility functions are also added or updated, such as a retry mechanism for inbox activity checks and a fixture loader for webhook secrets. Test environment setup and teardown logic is now handled in a dedicated index.js file. The overall structure is reorganized for clarity and maintainability, with logical separation of test step responsibilities.

Possibly related PRs

Suggested reviewers

  • allouis
✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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: 14

🧹 Nitpick comments (18)
features/step_definitions/like_steps.js (1)

27-42: Extract duplicated inbox fetching code into a helper function

This inbox fetching logic is duplicated in the "should not be liked" step below. Consider extracting it to a reusable helper function to avoid duplication.

+ async function fetchInboxAndFindObject(testContext, objectName) {
+   const response = await fetchActivityPub(
+     'http://fake-ghost-activitypub.test/.ghost/activitypub/inbox/index',
+     {
+       headers: {
+         Accept: 'application/ld+json',
+       },
+     },
+   );
+   const inbox = await response.json();
+   const object = testContext.objects[objectName];
+   const found = inbox.items.find((item) => item.object.id === object.id);
+   return { inbox, object, found };
+ }

  Then('the object {string} should be liked', async function (name) {
-   const response = await fetchActivityPub(
-     'http://fake-ghost-activitypub.test/.ghost/activitypub/inbox/index',
-     {
-       headers: {
-         Accept: 'application/ld+json',
-       },
-     },
-   );
-   const inbox = await response.json();
-   const object = this.objects[name];
-
-   const found = inbox.items.find((item) => item.object.id === object.id);
+   const { found } = await fetchInboxAndFindObject(this, name);
+   assert(found, `Object with ID ${object.id} not found in inbox`);
    assert(found.object.liked === true);
  });
features/support/activitypub.js (3)

3-51: Add function documentation and consider using iteration instead of recursion

This helper function implements a retry mechanism with exponential backoff, which is great for handling asynchronous operations. Consider adding JSDoc documentation and using iteration instead of recursion to avoid potential stack overflow issues with deep recursion.

/**
+ * Wait for an ActivityPub activity to appear in the inbox
+ * @param {Object} activity - The activity to wait for
+ * @param {Object|null} object - Optional object to further filter the activity
+ * @param {Object} options - Options for retry behavior
+ * @param {number} options.retryCount - Current retry count
+ * @param {number} options.delay - Current delay in ms
+ * @returns {Promise<void>} - Resolves when activity is found or throws error after max retries
+ */
export async function waitForInboxActivity(
    activity,
    object = null,
    options = {
        retryCount: 0,
        delay: 0,
    },
) {
    const MAX_RETRIES = 5;
+   // Use iteration instead of recursion
+   let currentRetry = options.retryCount;
+   let currentDelay = options.delay;
+   
+   while (currentRetry <= MAX_RETRIES) {
        const response = await fetchActivityPub(
            'http://fake-ghost-activitypub.test/.ghost/activitypub/inbox/index',
            {
                headers: {
                    Accept: 'application/ld+json',
                },
            },
        );
        const inbox = await response.json();

        if (
            inbox.items.find((item) => {
                const activityFound = item.id === activity.id;

                if (object) {
                    return activityFound && item.object.id === object.id;
                }

                return activityFound;
            })
        ) {
            return;
        }

-       if (options.retryCount === MAX_RETRIES) {
+       if (currentRetry === MAX_RETRIES) {
            throw new Error(
                `Max retries reached (${MAX_RETRIES}) when waiting on an activity in the inbox`,
            );
        }

-       if (options.delay > 0) {
-           await new Promise((resolve) => setTimeout(resolve, options.delay));
+       if (currentDelay > 0) {
+           await new Promise((resolve) => setTimeout(resolve, currentDelay));
        }

-       await waitForInboxActivity(activity, object, {
-           retryCount: options.retryCount + 1,
-           delay: options.delay + 500,
-       });
+       currentRetry++;
+       currentDelay += 500;
+   }
}

13-21: Add error handling for HTTP response

Consider adding error handling to check the HTTP response status before processing the JSON.

    const response = await fetchActivityPub(
        'http://fake-ghost-activitypub.test/.ghost/activitypub/inbox/index',
        {
            headers: {
                Accept: 'application/ld+json',
            },
        },
    );
+   if (!response.ok) {
+       throw new Error(`Failed to fetch inbox: ${response.status} ${response.statusText}`);
+   }
    const inbox = await response.json();

11-11: Consider making MAX_RETRIES configurable

The MAX_RETRIES constant is hardcoded. Consider making it configurable through the options parameter for more flexibility.

-   const MAX_RETRIES = 5;
+   const MAX_RETRIES = options.maxRetries || 5;
features/step_definitions/account_steps.js (2)

5-21: Add error handling for HTTP response and group assertions by category

Consider adding error handling for the HTTP response status and organizing assertions by category for better readability.

Then('the response contains {string} account details', async function (name) {
+   if (!this.response.ok) {
+       throw new Error(`Failed to get account details: ${this.response.status} ${this.response.statusText}`);
+   }
    const responseJson = await this.response.clone().json();
    const actor = name === 'Our' ? this.actors.Us : this.actors[name];

+   // String properties
    assert.equal(responseJson.name, actor.name);
    assert.equal(responseJson.handle, actor.handle);
    assert.equal(responseJson.bio, actor.summary);
    assert.equal(responseJson.url, actor.url);
    assert.equal(responseJson.avatarUrl, actor.icon?.url || '');
    assert.equal(responseJson.bannerImageUrl, actor.image?.url || '');
    
+   // Numeric properties
    assert.equal(typeof responseJson.postCount, 'number');
    assert.equal(typeof responseJson.likedCount, 'number');
    assert.equal(typeof responseJson.followingCount, 'number');
    assert.equal(typeof responseJson.followerCount, 'number');
    
+   // Boolean properties
    assert.equal(typeof responseJson.followedByMe, 'boolean');
    assert.equal(typeof responseJson.followsMe, 'boolean');
});

23-33: Add type checking for data table values

The current implementation assumes all values from the data table are strings. Consider adding type checking or conversion based on expected field types.

Then('the response contains the account details:', async function (data) {
+   if (!this.response.ok) {
+       throw new Error(`Failed to get account details: ${this.response.status} ${this.response.statusText}`);
+   }
    const responseJson = await this.response.clone().json();
+   const dataHash = data.rowsHash();

-   for (const [key, value] of Object.entries(data.rowsHash())) {
+   for (const [key, rawValue] of Object.entries(dataHash)) {
+       let expectedValue = rawValue;
+       
+       // Convert string values to appropriate types
+       if (rawValue === 'true') expectedValue = true;
+       else if (rawValue === 'false') expectedValue = false;
+       else if (!isNaN(rawValue) && rawValue !== '') expectedValue = Number(rawValue);
+       
        assert.equal(
            responseJson[key],
-           value,
-           `Expected ${key} to be "${value}" but got "${responseJson[key]}"`,
+           expectedValue,
+           `Expected ${key} to be "${expectedValue}" but got "${responseJson[key]}"`,
        );
    }
});
features/step_definitions/note_steps.js (2)

86-93: Add property existence check before assertion.

The assertions directly access nested properties without first verifying they exist, which could lead to confusing errors if the response structure is unexpected.

Then(
    'note {string} has the image URL {string}',
    function (activityName, expectedImageUrl) {
        const activity = this.activities[activityName];
+       assert(activity && activity.object && activity.object.attachment, 'Activity object should have an attachment');
        assert.equal(activity.object.attachment.url, expectedImageUrl);
        assert.equal(activity.object.attachment.type, 'Image');
    },
);

35-84: DRY opportunity: Similar code pattern in note creation steps.

Similar to the reply steps, there's duplicated code between the two note creation methods (with and without image).

Consider extracting common logic to a helper function:

import assert from 'node:assert';

import { Then, When } from '@cucumber/cucumber';

import { fetchActivityPub } from '../support/request.js';

+async function createNote(context, noteName, noteContent, imageUrl = null) {
+    const body = { content: noteContent };
+    
+    if (imageUrl) {
+        body.imageUrl = imageUrl;
+    }
+    
+    context.response = await fetchActivityPub(
+        'http://fake-ghost-activitypub.test/.ghost/activitypub/actions/note',
+        {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json',
+            },
+            body: JSON.stringify(body),
+        },
+    );
+    
+    if (context.response.ok) {
+        const activity = await context.response.clone().json();
+        context.activities[noteName] = activity;
+        context.objects[noteName] = activity.object;
+    }
+}

// ... existing code ...

When(
    'we create a note {string} with the content',
    async function (noteName, noteContent) {
+        await createNote(this, noteName, noteContent);
-        this.response = await fetchActivityPub(
-            'http://fake-ghost-activitypub.test/.ghost/activitypub/actions/note',
-            {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json',
-                },
-                body: JSON.stringify({
-                    content: noteContent,
-                }),
-            },
-        );
-
-        if (this.response.ok) {
-            const activity = await this.response.clone().json();
-
-            this.activities[noteName] = activity;
-            this.objects[noteName] = activity.object;
-        }
    },
);

When(
    'we create a note {string} with imageUrl {string} and content',
    async function (noteName, imageUrl, noteContent) {
+        await createNote(this, noteName, noteContent, imageUrl);
-        this.response = await fetchActivityPub(
-            'http://fake-ghost-activitypub.test/.ghost/activitypub/actions/note',
-            {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json',
-                },
-                body: JSON.stringify({
-                    content: noteContent,
-                    imageUrl: imageUrl,
-                }),
-            },
-        );
-
-        if (this.response.ok) {
-            const activity = await this.response.clone().json();
-
-            this.activities[noteName] = activity;
-            this.objects[noteName] = activity.object;
-        }
    },
);
features/step_definitions/request_steps.js (2)

68-87: Consider adding error handling for file operations.

The file reading operation could potentially fail, but there's no specific error handling. Adding a try/catch block would make debugging easier if there are issues with the test fixture file.

When(
    /^an authenticated (\"(post|put)\"\s)?request is made to "(.*)" with an image$/,
    async function (method, path) {
+       try {
            const image = await readFile(resolve(__dirname, '../fixtures/dog.jpg'));
    
            const formData = new FormData();
            formData.append(
                'file',
                new File([image], 'dog.jpg', { type: 'image/jpeg' }),
            );
    
            this.response = await fetchActivityPub(
                `http://fake-ghost-activitypub.test${path}`,
                {
                    method: method || 'post',
                    body: formData,
                },
            );
+       } catch (error) {
+           console.error('Error during image upload test:', error);
+           throw error;
+       }
    },
);

50-140: Well-structured HTTP request steps with good error messaging.

The remaining steps are implemented well with:

  • Clear separation of request types (authenticated, with data, with image, unauthenticated)
  • Detailed error messages in assertions
  • Proper handling of multipart form data for image uploads
  • Explicit response status code checks

As an additional improvement, consider extracting the common base URL as a constant:

import { fetchActivityPub } from '../support/request.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
+const BASE_URL = 'http://fake-ghost-activitypub.test';

// Then use ${BASE_URL} in place of 'http://fake-ghost-activitypub.test' throughout the file
features/step_definitions/webhook_steps.js (2)

80-90: Inconsistent media-type with the other two requests

The unsigned request uses application/ld+json, while the signed variants use application/json. Unless the endpoint actively distinguishes between the two, stick to a single content-type to avoid false negatives.


22-32: Nested-property helper does not handle array indices

authors.0.name or similar paths will be treated as a literal object key "0" instead of an array index, leading to shape mismatches. Consider a small enhancement that detects numeric segments and builds arrays when needed.

features/step_definitions/post_steps.js (1)

78-88: Only top-level keys are supported

post {n} has {key} set to {value} fails for nested paths such as author.name.
You can reuse the dotted-path reducer pattern already present in other step files:

-        assert(
-            String(post[key]) === String(value),
+        const actual = key.split('.').reduce((obj, k) => obj?.[k], post);
+        assert(
+            String(actual) === String(value),
             `Expected post ${postNumber} to have ${key} ${value}`,
         );
features/step_definitions/activitypub_steps.js (1)

246-266: Recursive polling risks stack growth – switch to a loop

waitForOutboxActivity performs up to five self-calls.
While the depth is small, replacing recursion with an iterative loop avoids needless stack frames and future “max call stack” surprises if the retry limit ever increases.

features/step_definitions/follow_steps.js (1)

134-190: Reduce code duplication in follower assertion steps

The three follower assertion steps have significant code duplication. They all fetch the followers list using identical code and then perform slightly different checks on it.

Extract the common functionality to a helper function:

async function getFollowers() {
    const response = await fetchActivityPub(
        'http://fake-ghost-activitypub.test/.ghost/activitypub/followers/index',
        {
            headers: {
                Accept: 'application/ld+json',
            },
        }
    );
    return await response.json();
}

Then('{string} is in our Followers', async function (actorName) {
    const followers = await getFollowers();
    const actor = this.actors[actorName];
    const found = (followers.orderedItems || []).find(
        (item) => item === actor.id
    );
    assert(found);
});

// Then update the other assertion steps similarly
features/step_definitions/repost_steps.js (1)

1-15: Good implementation of repost actions with a minor optimization opportunity.

The repost functionality is well implemented with proper encodeURIComponent usage for URL safety. However, I notice the hardcoded base URL appears in multiple places throughout this file.

Consider extracting the base URL to a constant at the top of the file to improve maintainability:

import assert from 'node:assert';

import { Then, When } from '@cucumber/cucumber';

import { fetchActivityPub } from '../support/request.js';

+const BASE_URL = 'http://fake-ghost-activitypub.test/.ghost/activitypub';
+
When('we repost the object {string}', async function (name) {
    const id = this.objects[name].id;
    this.response = await fetchActivityPub(
-        `http://fake-ghost-activitypub.test/.ghost/activitypub/actions/repost/${encodeURIComponent(id)}`,
+        `${BASE_URL}/actions/repost/${encodeURIComponent(id)}`,
        {
            method: 'POST',
        },
    );
});
features/step_definitions/reply_steps.js (2)

47-62: Consider extracting the hardcoded test ID.

The hardcoded ID for testing unknown objects could be better defined as a constant at the top of the file.

import { Given, When } from '@cucumber/cucumber';

import { fetchActivityPub } from '../support/request.js';

+const UNKNOWN_OBJECT_ID = 'http://fake-external-activitypub.test/note/123';
+
Given('{string} is a reply to {string}', async function (objectA, objectB) {
    this.objects[objectA].inReplyTo = this.objects[objectB].id;
});

...

When('we attempt to reply to an unknown object', async function () {
-    const id = 'http://fake-external-activitypub.test/note/123';
+    const id = UNKNOWN_OBJECT_ID;

    this.response = await fetchActivityPub(

64-117: DRY opportunity: Reduce duplication in reply creation steps.

The two reply steps (with and without image) share a lot of common code. Consider extracting the common logic to reduce duplication.

Create a helper function for reply creation:

import { Given, When } from '@cucumber/cucumber';

import { fetchActivityPub } from '../support/request.js';

+async function createReply(context, objectName, replyName, replyContent, imageUrl = null) {
+    const object = context.objects[objectName];
+    const body = { content: replyContent };
+    
+    if (imageUrl) {
+        body.imageUrl = imageUrl;
+    }
+    
+    context.response = await fetchActivityPub(
+        `http://fake-ghost-activitypub.test/.ghost/activitypub/actions/reply/${encodeURIComponent(object.id)}`,
+        {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json',
+            },
+            body: JSON.stringify(body),
+        },
+    );
+    
+    if (context.response.ok) {
+        const activity = await context.response.clone().json();
+        context.activities[replyName] = activity;
+        context.objects[replyName] = activity.object;
+    }
+}

...

When(
    'we reply {string} to {string} with the content',
    async function (replyName, objectName, replyContent) {
+        await createReply(this, objectName, replyName, replyContent);
-        const object = this.objects[objectName];
-
-        this.response = await fetchActivityPub(
-            `http://fake-ghost-activitypub.test/.ghost/activitypub/actions/reply/${encodeURIComponent(object.id)}`,
-            {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json',
-                },
-                body: JSON.stringify({
-                    content: replyContent,
-                }),
-            },
-        );
-
-        if (this.response.ok) {
-            const activity = await this.response.clone().json();
-
-            this.activities[replyName] = activity;
-            this.objects[replyName] = activity.object;
-        }
    },
);

When(
    'we reply {string} to {string} with imageUrl {string} and content',
    async function (replyName, objectName, imageUrl, replyContent) {
+        await createReply(this, objectName, replyName, replyContent, imageUrl);
-        const object = this.objects[objectName];
-
-        this.response = await fetchActivityPub(
-            `http://fake-ghost-activitypub.test/.ghost/activitypub/actions/reply/${encodeURIComponent(object.id)}`,
-            {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json',
-                },
-                body: JSON.stringify({
-                    content: replyContent,
-                    imageUrl: imageUrl,
-                }),
-            },
-        );
-
-        if (this.response.ok) {
-            const activity = await this.response.clone().json();
-
-            this.activities[replyName] = activity;
-            this.objects[replyName] = activity.object;
-        }
    },
);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2978c00 and 0f56008.

📒 Files selected for processing (18)
  • features/step_definitions/account_steps.js (1 hunks)
  • features/step_definitions/activitypub_steps.js (1 hunks)
  • features/step_definitions/feed_steps.js (1 hunks)
  • features/step_definitions/follow_steps.js (1 hunks)
  • features/step_definitions/index.js (1 hunks)
  • features/step_definitions/like_steps.js (1 hunks)
  • features/step_definitions/note_steps.js (1 hunks)
  • features/step_definitions/post_steps.js (1 hunks)
  • features/step_definitions/reply_steps.js (1 hunks)
  • features/step_definitions/repost_steps.js (1 hunks)
  • features/step_definitions/request_steps.js (1 hunks)
  • features/step_definitions/site_steps.js (1 hunks)
  • features/step_definitions/stepdefs.js (0 hunks)
  • features/step_definitions/thread_steps.js (1 hunks)
  • features/step_definitions/webhook_steps.js (1 hunks)
  • features/support/activitypub.js (1 hunks)
  • features/support/fixtures.js (2 hunks)
  • features/support/request.js (1 hunks)
💤 Files with no reviewable changes (1)
  • features/step_definitions/stepdefs.js
🧰 Additional context used
🧬 Code Graph Analysis (7)
features/step_definitions/thread_steps.js (8)
features/step_definitions/request_steps.js (3)
  • objectName (26-26)
  • responseJson (130-130)
  • object (28-29)
features/step_definitions/feed_steps.js (3)
  • responseJson (8-8)
  • responseJson (24-24)
  • object (26-26)
features/step_definitions/account_steps.js (2)
  • responseJson (6-6)
  • responseJson (24-24)
features/step_definitions/post_steps.js (10)
  • responseJson (8-8)
  • responseJson (31-31)
  • responseJson (52-52)
  • responseJson (64-64)
  • responseJson (80-80)
  • object (10-10)
  • object (33-33)
  • object (66-66)
  • post (67-67)
  • post (81-81)
features/step_definitions/like_steps.js (2)
  • object (37-37)
  • object (54-54)
features/step_definitions/reply_steps.js (4)
  • object (12-12)
  • object (30-30)
  • object (67-67)
  • object (94-94)
features/step_definitions/repost_steps.js (3)
  • object (27-27)
  • object (46-46)
  • object (74-74)
features/step_definitions/activitypub_steps.js (1)
  • object (23-27)
features/step_definitions/site_steps.js (2)
features/support/db.js (1)
  • getClient (6-22)
features/support/request.js (1)
  • fetchActivityPub (13-40)
features/step_definitions/like_steps.js (4)
features/step_definitions/reply_steps.js (5)
  • id (48-48)
  • object (12-12)
  • object (30-30)
  • object (67-67)
  • object (94-94)
features/step_definitions/repost_steps.js (14)
  • id (8-8)
  • id (55-55)
  • response (18-25)
  • response (37-44)
  • response (65-72)
  • inbox (26-26)
  • inbox (45-45)
  • inbox (73-73)
  • object (27-27)
  • object (46-46)
  • object (74-74)
  • found (29-29)
  • found (48-48)
  • found (75-75)
features/support/request.js (2)
  • fetchActivityPub (13-40)
  • found (52-52)
features/step_definitions/activitypub_steps.js (12)
  • response (453-460)
  • inbox (337-337)
  • inbox (366-366)
  • inbox (461-461)
  • object (23-27)
  • object (54-58)
  • found (247-247)
  • found (271-271)
  • found (309-311)
  • found (340-346)
  • found (368-379)
  • found (395-395)
features/support/activitypub.js (10)
features/step_definitions/feed_steps.js (2)
  • activity (25-25)
  • object (26-26)
features/step_definitions/note_steps.js (3)
  • activity (52-52)
  • activity (78-78)
  • activity (89-89)
features/step_definitions/follow_steps.js (3)
  • activity (102-102)
  • object (101-101)
  • response (105-111)
features/step_definitions/reply_steps.js (6)
  • activity (83-83)
  • activity (111-111)
  • object (12-12)
  • object (30-30)
  • object (67-67)
  • object (94-94)
features/step_definitions/post_steps.js (6)
  • activity (9-9)
  • activity (32-32)
  • activity (65-65)
  • object (10-10)
  • object (33-33)
  • object (66-66)
features/step_definitions/activitypub_steps.js (12)
  • activity (29-29)
  • activity (60-60)
  • activity (109-109)
  • activity (136-136)
  • activity (270-270)
  • activity (279-279)
  • MAX_RETRIES (246-246)
  • MAX_RETRIES (291-291)
  • response (453-460)
  • inbox (337-337)
  • inbox (366-366)
  • inbox (461-461)
features/step_definitions/like_steps.js (6)
  • object (37-37)
  • object (54-54)
  • response (28-35)
  • response (45-52)
  • inbox (36-36)
  • inbox (53-53)
features/step_definitions/repost_steps.js (9)
  • object (27-27)
  • object (46-46)
  • object (74-74)
  • response (18-25)
  • response (37-44)
  • response (65-72)
  • inbox (26-26)
  • inbox (45-45)
  • inbox (73-73)
features/step_definitions/request_steps.js (1)
  • object (28-29)
features/support/request.js (1)
  • fetchActivityPub (13-40)
features/step_definitions/post_steps.js (6)
features/step_definitions/feed_steps.js (5)
  • responseJson (8-8)
  • responseJson (24-24)
  • activity (25-25)
  • object (26-26)
  • found (27-27)
features/step_definitions/account_steps.js (2)
  • responseJson (6-6)
  • responseJson (24-24)
features/step_definitions/request_steps.js (2)
  • responseJson (130-130)
  • object (28-29)
features/step_definitions/thread_steps.js (4)
  • responseJson (8-8)
  • responseJson (23-23)
  • object (10-10)
  • post (11-11)
features/step_definitions/activitypub_steps.js (18)
  • activity (29-29)
  • activity (60-60)
  • activity (109-109)
  • activity (136-136)
  • activity (270-270)
  • activity (279-279)
  • activity (338-338)
  • activity (362-362)
  • activity (437-437)
  • object (23-27)
  • object (54-58)
  • object (102-102)
  • found (247-247)
  • found (271-271)
  • found (309-311)
  • found (340-346)
  • found (368-379)
  • found (395-395)
features/step_definitions/like_steps.js (4)
  • object (37-37)
  • object (54-54)
  • found (39-39)
  • found (56-56)
features/step_definitions/request_steps.js (8)
features/support/request.js (2)
  • __dirname (11-11)
  • fetchActivityPub (13-40)
features/step_definitions/feed_steps.js (1)
  • object (26-26)
features/step_definitions/like_steps.js (2)
  • object (37-37)
  • object (54-54)
features/step_definitions/reply_steps.js (4)
  • object (12-12)
  • object (30-30)
  • object (67-67)
  • object (94-94)
features/step_definitions/repost_steps.js (3)
  • object (27-27)
  • object (46-46)
  • object (74-74)
features/step_definitions/thread_steps.js (1)
  • object (10-10)
features/step_definitions/post_steps.js (3)
  • object (10-10)
  • object (33-33)
  • object (66-66)
features/step_definitions/activitypub_steps.js (1)
  • object (23-27)
features/step_definitions/webhook_steps.js (2)
features/support/fixtures.js (2)
  • createWebhookPost (396-420)
  • getWebhookSecret (424-433)
features/support/request.js (1)
  • fetchActivityPub (13-40)
🔇 Additional comments (38)
features/support/fixtures.js (3)

1-4: Good use of Node.js core module imports with proper namespace.

The code correctly imports Node.js core modules with the node: prefix, which is a good practice for clarity and to distinguish them from third-party modules.


9-10: Correctly defines ES module equivalents for CommonJS globals.

Since this is an ES module, the code properly defines __filename and __dirname variables which aren't available by default in ESM.


422-433: Well-implemented caching pattern for webhook secret.

The implementation follows a good singleton pattern that:

  1. Caches the webhook secret to avoid repeated file reads
  2. Uses proper path resolution with resolve(__dirname, ...)
  3. Provides a clean interface for other modules to access the secret

This approach is efficient and follows good practices for handling fixture data.

features/support/request.js (1)

4-4: Minor formatting improvement.

Added consistent blank line after imports to match the style used throughout the codebase.

features/step_definitions/site_steps.js (3)

1-5: Well-structured imports for step definition file.

The imports correctly bring in Cucumber step definition functions and the necessary support utilities.


6-10: Properly implemented database setup step.

This Given step:

  1. Clears the sites table to ensure tests start from a known state
  2. Resets the context's SITE_ID to null
  3. Follows the single responsibility principle by handling just database setup

This provides a clean foundation for site-related tests.


12-21: Clean implementation of site endpoint request.

This When step:

  1. Makes a proper ActivityPub request with appropriate headers
  2. Stores the response in the test context for later assertions
  3. Uses the shared fetchActivityPub utility consistently with other steps

The hardcoded URL is consistent with the testing approach used throughout the codebase.

features/step_definitions/thread_steps.js (3)

1-4: Appropriate imports for thread assertion steps.

The code correctly imports Node.js assert module and Cucumber's Then function for creating assertion steps.


5-20: Well-implemented post position verification in thread.

This step:

  1. Properly retrieves and parses the JSON response
  2. Gets the referenced object from the test context
  3. Extracts the post at the specified position (with 1-based indexing)
  4. Performs appropriate assertions with descriptive error messages
  5. Compares URLs/IDs to verify post identity

The implementation is consistent with the patterns used in other step files.


22-30: Clear and concise thread size verification.

This step:

  1. Correctly parses the JSON response
  2. Performs a simple but effective count verification
  3. Provides a detailed assertion message that includes both expected and actual counts

The implementation is straightforward and follows the established patterns in the test suite.

features/step_definitions/like_steps.js (1)

7-15: LGTM: Like operation implementation works correctly

The implementation correctly retrieves the object ID from test context and makes a POST request to the ActivityPub like action endpoint.

features/step_definitions/repost_steps.js (7)

1-15: Well-structured and concise repost implementation.

The repost step definition follows best practices by properly encoding the object ID in the URL path and using appropriate HTTP methods for the operation.


17-32: Good verification approach for repost status.

This step correctly fetches the inbox data and verifies the repost status using assertions. The code pattern aligns with other verification steps in the test suite.


34-52: Effective implementation for repost count verification.

The step definition follows the same pattern as the repost status check, properly asserting the exact repost count value.


54-78: Clear implementation of repost undoing and verification.

The step definitions for undoing a repost and verifying an object is not reposted follow consistent patterns and use appropriate assertions.


17-32: LGTM: Effective verification of repost state.

The implementation correctly fetches the inbox data and verifies the repost state by finding the matching object and checking its reposted property.


34-52: Well-structured assertion for repost count.

The step definition follows good practices for asynchronous code handling and assertion checking.


54-62: LGTM: Clear implementation for undoing reposts.

The implementation is straightforward and follows the same pattern as the repost action.

features/step_definitions/reply_steps.js (7)

5-7: Simple and effective reply relationship setup.

This step clearly establishes the reply relationship by setting the inReplyTo property on the object.


9-45: Well-structured error case handling for replies.

These step definitions correctly test error cases for replies, including attempts with no content and invalid content. The implementation follows good practices for API testing.


47-62: Good test for handling unknown objects.

This step properly tests the scenario of attempting to reply to an unknown object, which is an important edge case to verify.


64-89: Effective implementation for creating valid replies.

The step correctly handles the reply creation process and stores the response data in the test context for later verification, which is essential for maintaining test state.


91-117: Complete handling of replies with image attachments.

This step definition properly extends the basic reply functionality to include image URLs, following the same pattern as the text-only replies.


1-8: LGTM: Clean setup with appropriate imports.

The imports are clear and the "is a reply to" step definition properly establishes a relationship between objects.


9-45: Well-structured test cases for error scenarios.

Good implementation of test cases for attempting to create replies with invalid or missing content. The error cases are important to ensure robust handling of bad inputs.

features/step_definitions/note_steps.js (4)

7-33: Clear test cases for note creation error scenarios.

These steps appropriately test the error cases for note creation with no content and invalid content.


35-58: Clean implementation for creating notes with content.

The step definition properly handles note creation with content and stores the response data in the test context, following the established pattern used in other step files.


60-84: Well-structured implementation for notes with images.

This step correctly extends the basic note creation to include image URLs, maintaining consistency with the text-only note creation pattern.


1-34: LGTM: Well-structured test cases for note creation error handling.

The implementation correctly tests scenarios where note creation should fail due to invalid or missing content, which is important for validating error handling.

features/step_definitions/request_steps.js (6)

13-49: Good implementation of dynamic path resolution for authenticated requests.

The step definition handles complex scenarios where object IDs need to be resolved dynamically based on the test context. The implementation is robust and follows the established patterns in the test suite.


51-66: Clear handling of authenticated requests with JSON data.

This step definition properly handles requests with JSON data, converting the data table to a JSON object and sending it with the appropriate content type.


89-99: Clear implementation of unauthenticated requests.

This step correctly passes false as the third parameter to fetchActivityPub to make an unauthenticated request, which aligns with the function's implementation in the support code.


101-127: Well-structured response status verification steps.

These step definitions provide clear assertions with helpful error messages when verifying response statuses, which will make test failures more informative.


129-140: Thorough validation of file URL responses.

This step performs multiple validations on the file URL property, ensuring it exists, is a string, and has a valid format, which provides comprehensive verification.


1-12: LGTM: Appropriate imports and module setup.

The imports are relevant to the file's purpose and the ESM module path handling is correctly implemented.

features/step_definitions/follow_steps.js (3)

32-66: Inconsistent behavior between follow step definitions

The step we are following {string} performs a complete follow interaction including creating Accept activities and waiting for inbox processing, while we follow {string} only sends the follow request without waiting. This inconsistency could lead to confusion when writing tests.

Consider whether these steps should be consistent in their behavior or if the different names clearly indicate the different behaviors. If they should be consistent, update the simpler step to match the behavior of the more complete one.

Also applies to: 68-83


1-9: LGTM: Imports are well-organized

The imports are well-organized, with standard library imports first, followed by package imports, and then local imports. This follows good import organization practices.


125-132: LGTM: Clean implementation of data table handling

Nice implementation of the data table handling for creating multiple followers at once. This makes test writing more concise when setting up multiple followers.

Comment on lines +39 to +41
const found = inbox.items.find((item) => item.object.id === object.id);

assert(found.object.liked === true);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider handling case when object is not found in inbox

The assertion will throw an error if found is undefined (when the object isn't found in the inbox). Consider adding a more descriptive error message.

- assert(found.object.liked === true);
+ assert(found, `Object with ID ${object.id} not found in inbox`);
+ assert(found.object.liked === true, `Object is not liked as expected`);
📝 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 found = inbox.items.find((item) => item.object.id === object.id);
assert(found.object.liked === true);
const found = inbox.items.find((item) => item.object.id === object.id);
assert(found, `Object with ID ${object.id} not found in inbox`);
assert(found.object.liked === true, `Object is not liked as expected`);

Comment on lines +56 to +58
const found = inbox.items.find((item) => item.object.id === object.id);

assert(found.object.liked !== true);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider handling case when object is not found in inbox

Similar to the previous step, handle the case when the object isn't found in the inbox with a descriptive error message.

- assert(found.object.liked !== true);
+ assert(found, `Object with ID ${object.id} not found in inbox`);
+ assert(found.object.liked !== true, `Object is liked but should not be`);
📝 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 found = inbox.items.find((item) => item.object.id === object.id);
assert(found.object.liked !== true);
const found = inbox.items.find((item) => item.object.id === object.id);
assert(found, `Object with ID ${object.id} not found in inbox`);
assert(found.object.liked !== true, `Object is liked but should not be`);

Comment on lines +7 to +19
When('we request the feed with the next cursor', async function () {
const responseJson = await this.response.clone().json();
const nextCursor = responseJson.next;

this.response = await fetchActivityPub(
`http://fake-ghost-activitypub.test/.ghost/activitypub/feed/index?next=${encodeURIComponent(nextCursor)}`,
{
headers: {
Accept: 'application/json',
},
},
);
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for next cursor and HTTP response

Consider adding checks for the existence of nextCursor and for the HTTP response status before proceeding.

When('we request the feed with the next cursor', async function () {
    const responseJson = await this.response.clone().json();
    const nextCursor = responseJson.next;
+   
+   if (!nextCursor) {
+       throw new Error('No next cursor available in the response');
+   }

    this.response = await fetchActivityPub(
        `http://fake-ghost-activitypub.test/.ghost/activitypub/feed/index?next=${encodeURIComponent(nextCursor)}`,
        {
            headers: {
                Accept: 'application/json',
            },
        },
    );
+   
+   if (!this.response.ok) {
+       throw new Error(`Failed to fetch feed: ${this.response.status} ${this.response.statusText}`);
+   }
});
📝 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
When('we request the feed with the next cursor', async function () {
const responseJson = await this.response.clone().json();
const nextCursor = responseJson.next;
this.response = await fetchActivityPub(
`http://fake-ghost-activitypub.test/.ghost/activitypub/feed/index?next=${encodeURIComponent(nextCursor)}`,
{
headers: {
Accept: 'application/json',
},
},
);
});
When('we request the feed with the next cursor', async function () {
const responseJson = await this.response.clone().json();
const nextCursor = responseJson.next;
if (!nextCursor) {
throw new Error('No next cursor available in the response');
}
this.response = await fetchActivityPub(
`http://fake-ghost-activitypub.test/.ghost/activitypub/feed/index?next=${encodeURIComponent(nextCursor)}`,
{
headers: {
Accept: 'application/json',
},
},
);
if (!this.response.ok) {
throw new Error(`Failed to fetch feed: ${this.response.status} ${this.response.statusText}`);
}
});

Comment on lines +21 to +39
Then(
'the {string} in the feed has content {string}',
async function (activityOrObjectName, content) {
const responseJson = await this.response.clone().json();
const activity = this.activities[activityOrObjectName];
const object = this.objects[activityOrObjectName];
let found;

if (activity) {
found = responseJson.posts.find(
(post) => post.url === activity.object.id,
);
} else if (object) {
found = responseJson.posts.find((post) => post.url === object.id);
}

assert.equal(found.content, content);
},
);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for object not found in feed

The current implementation doesn't handle the case where the activity or object isn't found in the feed. Add appropriate error handling.

Then(
    'the {string} in the feed has content {string}',
    async function (activityOrObjectName, content) {
        const responseJson = await this.response.clone().json();
        const activity = this.activities[activityOrObjectName];
        const object = this.objects[activityOrObjectName];
        let found;

        if (activity) {
            found = responseJson.posts.find(
                (post) => post.url === activity.object.id,
            );
        } else if (object) {
            found = responseJson.posts.find((post) => post.url === object.id);
        }

+       if (!found) {
+           throw new Error(`Activity or object '${activityOrObjectName}' not found in feed`);
+       }

        assert.equal(found.content, content);
    },
);
📝 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
Then(
'the {string} in the feed has content {string}',
async function (activityOrObjectName, content) {
const responseJson = await this.response.clone().json();
const activity = this.activities[activityOrObjectName];
const object = this.objects[activityOrObjectName];
let found;
if (activity) {
found = responseJson.posts.find(
(post) => post.url === activity.object.id,
);
} else if (object) {
found = responseJson.posts.find((post) => post.url === object.id);
}
assert.equal(found.content, content);
},
);
Then(
'the {string} in the feed has content {string}',
async function (activityOrObjectName, content) {
const responseJson = await this.response.clone().json();
const activity = this.activities[activityOrObjectName];
const object = this.objects[activityOrObjectName];
let found;
if (activity) {
found = responseJson.posts.find(
(post) => post.url === activity.object.id,
);
} else if (object) {
found = responseJson.posts.find((post) => post.url === object.id);
}
if (!found) {
throw new Error(`Activity or object '${activityOrObjectName}' not found in feed`);
}
assert.equal(found.content, content);
},
);

Comment on lines +61 to +76
const payload = createWebhookPost();
const body = JSON.stringify(payload);
const timestamp = Date.now() - 60 * 60 * 1000; // An hour old
const hmac = createHmac('sha256', getWebhookSecret())
.update(body + timestamp)
.digest('hex');

this.response = await fetchActivityPub(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Ghost-Signature': `sha256=${hmac}, t=${timestamp}`,
},
body: body,
});
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

payloadData customisations are lost in the “old signature” path

The “old signature” step builds a fresh payload but forgets to merge in the scenario-specific overrides set in the previous Given step. Assertions against modified fields will therefore fail.

-        const payload = createWebhookPost();
+        let payload = createWebhookPost();
+        if (this.payloadData) {
+            payload = merge(payload, this.payloadData);
+        }
📝 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 payload = createWebhookPost();
const body = JSON.stringify(payload);
const timestamp = Date.now() - 60 * 60 * 1000; // An hour old
const hmac = createHmac('sha256', getWebhookSecret())
.update(body + timestamp)
.digest('hex');
this.response = await fetchActivityPub(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Ghost-Signature': `sha256=${hmac}, t=${timestamp}`,
},
body: body,
});
},
// build an hour-old signature, preserving any scenario-specific overrides
- const payload = createWebhookPost();
+ let payload = createWebhookPost();
+ if (this.payloadData) {
+ payload = merge(payload, this.payloadData);
+ }
const body = JSON.stringify(payload);
const timestamp = Date.now() - 60 * 60 * 1000; // An hour old
const hmac = createHmac('sha256', getWebhookSecret())
.update(body + timestamp)
.digest('hex');
this.response = await fetchActivityPub(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Ghost-Signature': `sha256=${hmac}, t=${timestamp}`,
},
body: body,
});
},

Comment on lines +10 to +30
async function getActor(input) {
const existingActor = this.actors[input];

let type = 'Person';
let name = input;

if (!existingActor) {
const parsed = parseActorString(input);
if (parsed.type && parsed.name) {
type = parsed.type;
name = parsed.name;
}
this.actors[name] = await createActor(name, { type });
}

return {
type,
name,
actor: this.actors[name],
};
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Inconsistent type handling in getActor function

The function correctly parses actor type from input strings, but has a potential issue with returning the correct type for existing actors. When an actor already exists in this.actors, the function still initializes type to 'Person' but doesn't update it based on the existing actor's actual type.

Consider modifying the function to extract type information from existing actors:

async function getActor(input) {
    const existingActor = this.actors[input];

    let type = 'Person';
    let name = input;

    if (!existingActor) {
        const parsed = parseActorString(input);
        if (parsed.type && parsed.name) {
            type = parsed.type;
            name = parsed.name;
        }
        this.actors[name] = await createActor(name, { type });
+    } else {
+        // Extract type from existing actor if possible
+        type = existingActor.type || type;
    }

    return {
        type,
        name,
        actor: this.actors[name],
    };
}
📝 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
async function getActor(input) {
const existingActor = this.actors[input];
let type = 'Person';
let name = input;
if (!existingActor) {
const parsed = parseActorString(input);
if (parsed.type && parsed.name) {
type = parsed.type;
name = parsed.name;
}
this.actors[name] = await createActor(name, { type });
}
return {
type,
name,
actor: this.actors[name],
};
}
async function getActor(input) {
const existingActor = this.actors[input];
let type = 'Person';
let name = input;
if (!existingActor) {
const parsed = parseActorString(input);
if (parsed.type && parsed.name) {
type = parsed.type;
name = parsed.name;
}
this.actors[name] = await createActor(name, { type });
} else {
// Extract type from existing actor if possible
type = existingActor.type || type;
}
return {
type,
name,
actor: this.actors[name],
};
}

Comment on lines +94 to +97
this.activities[`Unfollow(${name})`] = await this.response
.clone()
.json();
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Potential JSON parsing error for non-JSON responses

The we unfollow {string} step attempts to parse the response as JSON without checking if the response actually contains JSON data. If the server returns a non-JSON response (like plain text or HTML), this will throw an error.

Add a check for the content type or handle potential parsing errors:

    if (this.response.ok) {
-        this.activities[`Unfollow(${name})`] = await this.response
-            .clone()
-            .json();
+        try {
+            this.activities[`Unfollow(${name})`] = await this.response
+                .clone()
+                .json();
+        } catch (e) {
+            // Handle case where response isn't JSON
+            this.activities[`Unfollow(${name})`] = { 
+                status: 'unfollow_sent', 
+                responseStatus: this.response.status 
+            };
+        }
    }
📝 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
this.activities[`Unfollow(${name})`] = await this.response
.clone()
.json();
}
if (this.response.ok) {
try {
this.activities[`Unfollow(${name})`] = await this.response
.clone()
.json();
} catch (e) {
// Handle case where response isn't JSON
this.activities[`Unfollow(${name})`] = {
status: 'unfollow_sent',
responseStatus: this.response.status
};
}
}

Comment on lines +64 to +78
Then('the object {string} should not be reposted', async function (name) {
const response = await fetchActivityPub(
'http://fake-ghost-activitypub.test/.ghost/activitypub/inbox/index',
{
headers: {
Accept: 'application/ld+json',
},
},
);
const inbox = await response.json();
const object = this.objects[name];
const found = inbox.items.find((item) => item.object.id === object.id);

assert(found.object.reposted !== true);
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider handling potential null/undefined object.

The implementation assumes that found will always exist in the inbox items. If the object is not found, this will result in a null reference error.

Add a null check before accessing the object properties:

    const inbox = await response.json();
    const object = this.objects[name];
    const found = inbox.items.find((item) => item.object.id === object.id);

-    assert(found.object.reposted !== true);
+    assert(found && found.object.reposted !== true, 'Object not found in inbox or is still reposted');
📝 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
Then('the object {string} should not be reposted', async function (name) {
const response = await fetchActivityPub(
'http://fake-ghost-activitypub.test/.ghost/activitypub/inbox/index',
{
headers: {
Accept: 'application/ld+json',
},
},
);
const inbox = await response.json();
const object = this.objects[name];
const found = inbox.items.find((item) => item.object.id === object.id);
assert(found.object.reposted !== true);
});
Then('the object {string} should not be reposted', async function (name) {
const response = await fetchActivityPub(
'http://fake-ghost-activitypub.test/.ghost/activitypub/inbox/index',
{
headers: {
Accept: 'application/ld+json',
},
},
);
const inbox = await response.json();
const object = this.objects[name];
const found = inbox.items.find((item) => item.object.id === object.id);
assert(
found && found.object.reposted !== true,
'Object not found in inbox or is still reposted'
);
});

Comment on lines +86 to +93
Then(
'note {string} has the image URL {string}',
function (activityName, expectedImageUrl) {
const activity = this.activities[activityName];
assert.equal(activity.object.attachment.url, expectedImageUrl);
assert.equal(activity.object.attachment.type, 'Image');
},
);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add defensive check for attachment property.

The assertion assumes that activity.object.attachment exists and has url and type properties, but doesn't verify this structure first.

Add defensive checks:

Then(
    'note {string} has the image URL {string}',
    function (activityName, expectedImageUrl) {
        const activity = this.activities[activityName];
+        assert(activity.object.attachment, 'Activity object should have an attachment');
+        assert(typeof activity.object.attachment === 'object', 'Attachment should be an object');
        assert.equal(activity.object.attachment.url, expectedImageUrl);
        assert.equal(activity.object.attachment.type, 'Image');
    },
);
📝 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
Then(
'note {string} has the image URL {string}',
function (activityName, expectedImageUrl) {
const activity = this.activities[activityName];
assert.equal(activity.object.attachment.url, expectedImageUrl);
assert.equal(activity.object.attachment.type, 'Image');
},
);
Then(
'note {string} has the image URL {string}',
function (activityName, expectedImageUrl) {
const activity = this.activities[activityName];
assert(activity.object.attachment, 'Activity object should have an attachment');
assert(typeof activity.object.attachment === 'object', 'Attachment should be an object');
assert.equal(activity.object.attachment.url, expectedImageUrl);
assert.equal(activity.object.attachment.type, 'Image');
},
);

Comment on lines +13 to +46
When(
/an authenticated (\"(delete|get|post|put)\"\s)?request is made to "(.*)"$/,
async function (method, path) {
const requestMethod = method || 'get';
let requestPath = path;

// If this is a request to the /thread/ or delete post endpoint, we need to replace the
// object name with the object ID as we don't have a way to know the object
// ID ahead of time
if (
path.includes('/thread/') ||
(requestMethod === 'delete' && path.includes('/post/'))
) {
const objectName = path.split('/').pop(); // Object name is the last part of the path

const object =
this.objects[objectName] || this.activities[objectName]?.object;

if (object) {
requestPath = path.replace(
objectName,
encodeURIComponent(object.id),
);
}
}

this.response = await fetchActivityPub(
`http://fake-ghost-activitypub.test${requestPath}`,
{
method: requestMethod,
headers: {
Accept: 'application/ld+json',
},
},
);
},
);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Potential issue with request method default value.

The step definition pattern for the HTTP method has a potential issue. The regex captures a string like "get" (with quotes), but the default value is just 'get' (without quotes).

Fix the inconsistency in how the method is used:

When(
    /an authenticated (\"(delete|get|post|put)\"\s)?request is made to "(.*)"$/,
    async function (method, path) {
-        const requestMethod = method || 'get';
+        // Extract the actual method from the captured group or default to 'get'
+        const requestMethod = method ? method.replace(/["\s]/g, '') : 'get';
        let requestPath = path;

        // ...rest of the function

Also, the URL construction is repeated throughout the file - consider extracting the base URL as a constant.

no refs

Refactored cucumber steps into separate files, grouped by either feature or
functionality (for generic steps). `stepdefs.js` has been renamed to `index.js`
and contains the hooks to execute as well as any global setup
@mike182uk mike182uk force-pushed the cucumber-split-up-steps branch from 0f56008 to 47c4a6d Compare May 1, 2025 20:26
@mike182uk mike182uk requested a review from allouis May 1, 2025 20:28
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: 3

♻️ Duplicate comments (6)
features/step_definitions/repost_steps.js (6)

75-77: ⚠️ Potential issue

Consider handling potential null/undefined object.

The implementation assumes that found will always exist in the inbox items. If the object is not found, this will result in a null reference error.

    const object = this.objects[name];
    const found = inbox.items.find((item) => item.object.id === object.id);

-    assert(found.object.reposted !== true);
+    assert(found && found.object.reposted !== true, 'Object not found in inbox or is still reposted');

29-31: ⚠️ Potential issue

Add null check before accessing found.object.

If the object is not found in the inbox items, this will result in a TypeError when trying to access found.object.

    const object = this.objects[name];
    const found = inbox.items.find((item) => item.object.id === object.id);

-    assert(found.object.reposted === true);
+    assert(found && found.object.reposted === true, 'Object not found in inbox or is not reposted');

48-50: ⚠️ Potential issue

Add null check before accessing found.object.

If the object is not found in the inbox items, this will result in a TypeError when trying to access found.object.

    const found = inbox.items.find((item) => item.object.id === object.id);

-    assert(found.object.repostCount === repostCount);
+    assert(found && found.object.repostCount === repostCount, `Object not found in inbox or repost count doesn't match ${repostCount}`);

76-77: Consider handling potential null/undefined object.

The implementation assumes that found will always exist in the inbox items. If the object is not found, this will result in a null reference error.

Add a null check before accessing the object properties:

    const found = inbox.items.find((item) => item.object.id === object.id);

-    assert(found.object.reposted !== true);
+    assert(found && found.object.reposted !== true, 'Object not found in inbox or is still reposted');

29-31: ⚠️ Potential issue

Handle potential null/undefined object.

The implementation assumes that found will always exist in the inbox items. If the object is not found, this will result in a null reference error.

Add a null check before accessing the object properties:

    const found = inbox.items.find((item) => item.object.id === object.id);

-    assert(found.object.reposted === true);
+    assert(found && found.object.reposted === true, 'Object not found in inbox or is not reposted');

48-50: ⚠️ Potential issue

Handle potential null/undefined object.

Similar to the issue found in other assertions, this code assumes that found will always exist in the inbox items. If the object is not found, this will result in a null reference error.

Add a null check before accessing the object properties:

    const found = inbox.items.find((item) => item.object.id === object.id);

-    assert(found.object.repostCount === repostCount);
+    assert(found && found.object.repostCount === repostCount, `Object not found in inbox or does not have expected repost count of ${repostCount}`);
🧹 Nitpick comments (9)
features/step_definitions/activitypub_steps.js (9)

165-165: Fix typo in variable name.

There's a typo in the variable name: "Reponse" should be "Response".

-    const firstPageReponse = await fetchActivityPub(
+    const firstPageResponse = await fetchActivityPub(

229-229: Fix typo in variable name.

The same typo appears in another instance.

-    const firstPageReponse = await fetchActivityPub(initialResponseJson.first, {
+    const firstPageResponse = await fetchActivityPub(initialResponseJson.first, {

302-302: Fix typo in variable name.

The same typo appears in a third instance.

-    const firstPageReponse = await fetchActivityPub(initialResponseJson.first, {
+    const firstPageResponse = await fetchActivityPub(initialResponseJson.first, {

239-267: Consider using a loop instead of recursion for retries.

Using recursion for retrying operations can lead to call stack issues and is generally less efficient than using a loop. Consider refactoring to use a while loop instead.

-async function waitForOutboxActivity(
-    activity,
-    options = {
-        retryCount: 0,
-        delay: 0,
-    },
-) {
-    const MAX_RETRIES = 5;
-    const found = await findInOutbox(activity);
-
-    if (found) {
-        return;
-    }
-
-    if (options.retryCount >= MAX_RETRIES) {
-        throw new Error(
-            `Max retries reached (${MAX_RETRIES}) when waiting on an activity in the outbox`,
-        );
-    }
-
-    if (options.delay > 0) {
-        await new Promise((resolve) => setTimeout(resolve, options.delay));
-    }
-
-    await waitForOutboxActivity(activity, {
-        retryCount: options.retryCount + 1,
-        delay: options.delay + 500,
-    });
-}
+async function waitForOutboxActivity(activity) {
+    const MAX_RETRIES = 5;
+    let retryCount = 0;
+    let delay = 0;
+    
+    while (retryCount <= MAX_RETRIES) {
+        const found = await findInOutbox(activity);
+        
+        if (found) {
+            return;
+        }
+        
+        if (retryCount >= MAX_RETRIES) {
+            throw new Error(
+                `Max retries reached (${MAX_RETRIES}) when waiting on an activity in the outbox`,
+            );
+        }
+        
+        if (delay > 0) {
+            await new Promise((resolve) => setTimeout(resolve, delay));
+        }
+        
+        retryCount++;
+        delay += 500;
+    }
+}

283-331: Consider using a loop instead of recursion for retries.

Similar to the previous function, this one also uses recursion for retry logic. Consider refactoring to use a while loop for better efficiency and to avoid potential call stack issues.

-async function waitForOutboxActivityType(
-    activityType,
-    objectType,
-    options = {
-        retryCount: 0,
-        delay: 0,
-    },
-) {
-    const MAX_RETRIES = 5;
-
-    const initialResponse = await fetchActivityPub(
-        'http://fake-ghost-activitypub.test/.ghost/activitypub/outbox/index',
-        {
-            headers: {
-                Accept: 'application/ld+json',
-            },
-        },
-    );
-    const initialResponseJson = await initialResponse.json();
-    const firstPageReponse = await fetchActivityPub(initialResponseJson.first, {
-        headers: {
-            Accept: 'application/ld+json',
-        },
-    });
-    const outbox = await firstPageReponse.json();
-
-    const found = (outbox.orderedItems || []).find((item) => {
-        return item.type === activityType && item.object?.type === objectType;
-    });
-
-    if (found) {
-        return found;
-    }
-
-    if (options.retryCount === MAX_RETRIES) {
-        throw new Error(
-            `Max retries reached (${MAX_RETRIES}) when waiting for ${activityType}(${objectType}) in the outbox`,
-        );
-    }
-
-    if (options.delay > 0) {
-        await new Promise((resolve) => setTimeout(resolve, options.delay));
-    }
-
-    return waitForOutboxActivityType(activityType, objectType, {
-        retryCount: options.retryCount + 1,
-        delay: options.delay + 500,
-    });
-}
+async function waitForOutboxActivityType(activityType, objectType) {
+    const MAX_RETRIES = 5;
+    let retryCount = 0;
+    let delay = 0;
+    
+    while (retryCount <= MAX_RETRIES) {
+        const initialResponse = await fetchActivityPub(
+            'http://fake-ghost-activitypub.test/.ghost/activitypub/outbox/index',
+            {
+                headers: {
+                    Accept: 'application/ld+json',
+                },
+            },
+        );
+        const initialResponseJson = await initialResponse.json();
+        const firstPageResponse = await fetchActivityPub(initialResponseJson.first, {
+            headers: {
+                Accept: 'application/ld+json',
+            },
+        });
+        const outbox = await firstPageResponse.json();
+    
+        const found = (outbox.orderedItems || []).find((item) => {
+            return item.type === activityType && item.object?.type === objectType;
+        });
+    
+        if (found) {
+            return found;
+        }
+    
+        if (retryCount === MAX_RETRIES) {
+            throw new Error(
+                `Max retries reached (${MAX_RETRIES}) when waiting for ${activityType}(${objectType}) in the outbox`,
+            );
+        }
+    
+        if (delay > 0) {
+            await new Promise((resolve) => setTimeout(resolve, delay));
+        }
+        
+        retryCount++;
+        delay += 500;
+    }
+    
+    // This line should not be reached due to the error throw above, but TypeScript might require it
+    return null;
+}

151-191: Consider refactoring collection fetching logic into a utility function.

There's duplicated logic for fetching and parsing ActivityPub collections from various endpoints across this file. Consider extracting it into a reusable utility function to reduce code duplication.

You could create a utility function like:

// Add to support/activitypub.js
export async function fetchCollection(collectionType) {
    const initialResponse = await fetchActivityPub(
        `http://fake-ghost-activitypub.test/.ghost/activitypub/${collectionType}/index`,
        {
            headers: {
                Accept: 'application/ld+json',
            },
        },
    );
    const initialResponseJson = await initialResponse.json();

    let collection = initialResponseJson;

    if (initialResponseJson.first) {
        const firstPageResponse = await fetchActivityPub(
            initialResponseJson.first,
            {
                headers: {
                    Accept: 'application/ld+json',
                },
            },
        );
        collection = await firstPageResponse.json();
    }

    return collection;
}

Then use it in your step definitions:

import { fetchCollection } from '../support/activitypub.js';

async function getObjectInCollection(objectName, collectionType) {
    const collection = await fetchCollection(collectionType);
    const object = this.objects[objectName] || this.actors[objectName];
    
    if (!object) {
        throw new Error(`Could not find object or actor with name "${objectName}"`);
    }

    return (collection.orderedItems || []).find((item) => {
        // existing logic
    });
}

246-251: Consider adding a null/undefined check for activity.

The function is checking if the activity was found but doesn't verify if the activity parameter itself is valid before calling findInOutbox.

Add a check at the beginning of the function to ensure the activity parameter is valid:

async function waitForOutboxActivity(
    activity,
    options = {
        retryCount: 0,
        delay: 0,
    },
) {
    const MAX_RETRIES = 5;
+    if (!activity) {
+        throw new Error('Activity parameter is undefined or null');
+    }
    const found = await findInOutbox(activity);

    if (found) {
        return;
    }

176-191: Add validation for object existence.

The function doesn't verify if object exists after attempting to retrieve it, which could lead to errors if a non-existent object name is provided.

Add a check to ensure the object exists before proceeding:

    const object = this.objects[objectName] || this.actors[objectName];

+    if (!object) {
+        return null;
+    }

    return (collection.orderedItems || []).find((item) => {

253-267: Consider extracting the MAX_RETRIES value to a configurable constant.

The MAX_RETRIES value is hardcoded in multiple places within the file. This makes it harder to maintain and change if requirements change.

Extract it to a constant at the top level of the file or to a shared configuration:

import { waitForInboxActivity } from '../support/activitypub.js';
import {
    createActivity,
    createActor,
    createObject,
} from '../support/fixtures.js';
import { fetchActivityPub } from '../support/request.js';
import { waitForRequest } from '../support/request.js';
import { parseActivityString, parseActorString } from '../support/steps.js';

+// Configuration
+const MAX_RETRY_ATTEMPTS = 5;

async function activityCreatedBy(activityDef, name, actorName) {

And then update all occurrences:

    if (options.retryCount >= MAX_RETRIES) {
        throw new Error(
-            `Max retries reached (${MAX_RETRIES}) when waiting on an activity in the outbox`,
+            `Max retries reached (${MAX_RETRY_ATTEMPTS}) when waiting on an activity in the outbox`,
        );
    }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0f56008 and 47c4a6d.

📒 Files selected for processing (19)
  • features/step_definitions/account_steps.js (1 hunks)
  • features/step_definitions/activitypub_steps.js (1 hunks)
  • features/step_definitions/feed_steps.js (1 hunks)
  • features/step_definitions/follow_steps.js (1 hunks)
  • features/step_definitions/index.js (1 hunks)
  • features/step_definitions/like_steps.js (1 hunks)
  • features/step_definitions/note_steps.js (1 hunks)
  • features/step_definitions/post_steps.js (1 hunks)
  • features/step_definitions/reply_steps.js (1 hunks)
  • features/step_definitions/repost_steps.js (1 hunks)
  • features/step_definitions/request_steps.js (1 hunks)
  • features/step_definitions/site_steps.js (1 hunks)
  • features/step_definitions/stepdefs.js (0 hunks)
  • features/step_definitions/thread_steps.js (1 hunks)
  • features/step_definitions/webhook_steps.js (1 hunks)
  • features/support/activitypub.js (1 hunks)
  • features/support/fixtures.js (2 hunks)
  • features/support/path.js (1 hunks)
  • features/support/request.js (1 hunks)
💤 Files with no reviewable changes (1)
  • features/step_definitions/stepdefs.js
✅ Files skipped from review due to trivial changes (2)
  • features/support/path.js
  • features/step_definitions/index.js
🚧 Files skipped from review as they are similar to previous changes (14)
  • features/support/request.js
  • features/step_definitions/site_steps.js
  • features/step_definitions/thread_steps.js
  • features/step_definitions/feed_steps.js
  • features/support/fixtures.js
  • features/support/activitypub.js
  • features/step_definitions/reply_steps.js
  • features/step_definitions/like_steps.js
  • features/step_definitions/account_steps.js
  • features/step_definitions/request_steps.js
  • features/step_definitions/post_steps.js
  • features/step_definitions/note_steps.js
  • features/step_definitions/webhook_steps.js
  • features/step_definitions/follow_steps.js

collection = await firstPageReponse.json();
}

const object = this.objects[objectName] || this.actors[objectName];
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add check for object existence.

The function assumes that the object exists in either this.objects or this.actors, but there's no check for this case. If the object is not found, this could lead to unexpected behavior.

    const object = this.objects[objectName] || this.actors[objectName];
+   
+   if (!object) {
+       throw new Error(`Could not find object or actor with name "${objectName}"`);
+   }
📝 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 object = this.objects[objectName] || this.actors[objectName];
const object = this.objects[objectName] || this.actors[objectName];
if (!object) {
throw new Error(`Could not find object or actor with name "${objectName}"`);
}

Comment on lines +397 to +400
if (!this.found) {
this.found = {};
}
this.found[string] = found;
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Initialize this.found at test context creation.

The object this.found is conditionally initialized in this step, which could cause issues if other parts of the code try to access it before this condition is executed. Consider initializing it when the test context is created.


🏁 Script executed:

#!/bin/bash
# Check if 'this.found' is initialized elsewhere in a World or index.js file

echo "Checking for 'this.found' initialization in index.js or world.js files..."
fd -g "index.js" -g "world.js" -p features/step_definitions | xargs grep -l "this.found" 2>/dev/null || echo "Not found in world or index files"

echo "\nChecking for access to 'this.found' in other step files..."
rg "this\.found" features/step_definitions/ | grep -v "if (!this.found)"

Length of output: 1000


🏁 Script executed:

#!/bin/bash
# Locate any world or index files in the repository
echo "Searching for world.js or index.js across the repo..."
fd -g "world.js" -g "index.js"

echo -e "\nSearching for Cucumber world constructor usage (setWorldConstructor)..."
rg "setWorldConstructor" -g "*.js" || echo "No world constructor found"

echo -e "\nSearching for Before hooks in features/support..."
rg "Before\(" -p features/support || echo "No Before hooks found in features/support"

Length of output: 742


Initialize this.found for every scenario

To avoid conditional setup in individual steps and ensure this.found is always available, move its initialization into a Cucumber World constructor or a Before hook.

Suggested changes:

  • Create (or update) a support file, e.g. features/support/hooks.js or features/support/world.js

  • Add:

    const { Before } = require('@cucumber/cucumber');
    
    Before(function () {
      this.found = {};
    });
  • In features/step_definitions/activitypub_steps.js, you can then remove the if (!this.found) guard since this.found will always be defined.

This ensures this.found is reset and available at the start of each scenario.

Comment on lines +317 to +321
if (options.retryCount === MAX_RETRIES) {
throw new Error(
`Max retries reached (${MAX_RETRIES}) when waiting for ${activityType}(${objectType}) in the outbox`,
);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix inconsistent retry condition check.

There's an inconsistency in how the retry count is checked. On line 253, the condition is options.retryCount >= MAX_RETRIES but here it's using strict equality. This could lead to different behavior between the two retry mechanisms.

Make the condition check consistent with the pattern used in the waitForOutboxActivity function:

-    if (options.retryCount === MAX_RETRIES) {
+    if (options.retryCount >= MAX_RETRIES) {
        throw new Error(
            `Max retries reached (${MAX_RETRIES}) when waiting for ${activityType}(${objectType}) in the outbox`,
        );
    }
📝 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
if (options.retryCount === MAX_RETRIES) {
throw new Error(
`Max retries reached (${MAX_RETRIES}) when waiting for ${activityType}(${objectType}) in the outbox`,
);
}
if (options.retryCount >= MAX_RETRIES) {
throw new Error(
`Max retries reached (${MAX_RETRIES}) when waiting for ${activityType}(${objectType}) in the outbox`,
);
}

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: 6

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

68-82: Potential TypeError when found is undefined
A previous review already pointed this out; the risk still exists.

features/step_definitions/activitypub_steps.js (1)

271-276: Make retry condition consistent (>=)

This mirrors the inbox helper; using === here is inconsistent and may under-count retries if MAX_RETRIES is decreased.

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

43-51: Cap exponential back-off delay

Delay increases un-boundedly: 0 ms → 500 ms → 1 s → 1.5 s → 2 s → 2.5 s…
If MAX_RETRIES is ever raised, waits could become excessive. Consider capping or switching to a fixed delay.

features/step_definitions/activitypub_steps.js (1)

155-195: Guard against missing objects before dereferencing

getObjectInCollection silently proceeds when object is undefined. Down-stream, object.id will throw.

-    const object = this.objects[objectName] || this.actors[objectName];
+    const object =
+        this.objects[objectName] || this.actors[objectName] || null;
+
+    if (!object) {
+        throw new Error(`Unknown object or actor "${objectName}"`);
+    }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 47c4a6d and 2e86713.

📒 Files selected for processing (3)
  • features/step_definitions/activitypub_steps.js (1 hunks)
  • features/step_definitions/repost_steps.js (1 hunks)
  • features/support/activitypub.js (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Build, Test and Push
🔇 Additional comments (1)
features/step_definitions/repost_steps.js (1)

110-128: this.objects.Note may be undefined – please verify

Hard-coding 'Note' assumes such an object was stored under that exact key in every scenario.
If the feature uses a different alias, object will be undefined and the matching logic will degrade to a type-check only.

Please confirm the intent or fall back to objectNameOrType as is done elsewhere.

Comment on lines +13 to +22
const response = await fetchActivityPub(
'http://fake-ghost-activitypub.test/.ghost/activitypub/inbox/index',
{
headers: {
Accept: 'application/ld+json',
},
},
);
const inbox = await response.json();

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Check response.ok before parsing JSON

fetchActivityPub will happily return a non-2xx response that still yields a body.
Blindly calling response.json() may mask HTTP errors and make test failures hard to interpret.

-    const inbox = await response.json();
+    if (!response.ok) {
+        throw new Error(
+            `Inbox request failed: ${response.status} ${response.statusText}`,
+        );
+    }
+    const inbox = await response.json();
📝 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 response = await fetchActivityPub(
'http://fake-ghost-activitypub.test/.ghost/activitypub/inbox/index',
{
headers: {
Accept: 'application/ld+json',
},
},
);
const inbox = await response.json();
const response = await fetchActivityPub(
'http://fake-ghost-activitypub.test/.ghost/activitypub/inbox/index',
{
headers: {
Accept: 'application/ld+json',
},
},
);
if (!response.ok) {
throw new Error(
`Inbox request failed: ${response.status} ${response.statusText}`,
);
}
const inbox = await response.json();

Comment on lines +37 to +41
if (options.retryCount === MAX_RETRIES) {
throw new Error(
`Max retries reached (${MAX_RETRIES}) when waiting on an activity in the inbox`,
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Retry guard should be >= not ===

The same helper later uses >=. Using strict equality here means an extra retry might sneak through when MAX_RETRIES changes.

-    if (options.retryCount === MAX_RETRIES) {
+    if (options.retryCount >= MAX_RETRIES) {
📝 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
if (options.retryCount === MAX_RETRIES) {
throw new Error(
`Max retries reached (${MAX_RETRIES}) when waiting on an activity in the inbox`,
);
}
if (options.retryCount >= MAX_RETRIES) {
throw new Error(
`Max retries reached (${MAX_RETRIES}) when waiting on an activity in the inbox`,
);
}

Comment on lines +53 to +70
export async function findInOutbox(activity) {
const initialResponse = await fetchActivityPub(
'http://fake-ghost-activitypub.test/.ghost/activitypub/outbox/index',
{
headers: {
Accept: 'application/ld+json',
},
},
);
const initialResponseJson = await initialResponse.json();
const firstPageReponse = await fetchActivityPub(initialResponseJson.first, {
headers: {
Accept: 'application/ld+json',
},
});
const outbox = await firstPageReponse.json();

return (outbox.orderedItems || []).find((item) => item.id === activity.id);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Minor issues in findInOutbox

  1. Typo: firstPageReponsefirstPageResponse.
  2. No response.ok checks (same concern as above).
  3. Only the first page is inspected; large outboxes paginate. Either iterate through next links or document the limitation.
-    const firstPageReponse = await fetchActivityPub(initialResponseJson.first, {
+    const firstPageResponse = await fetchActivityPub(initialResponseJson.first, {
         headers: { Accept: 'application/ld+json' },
     });
-    const outbox = await firstPageReponse.json();
+    if (!firstPageResponse.ok) {
+        throw new Error(`Outbox request failed: ${firstPageResponse.status}`);
+    }
+    const outbox = await firstPageResponse.json();
📝 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
export async function findInOutbox(activity) {
const initialResponse = await fetchActivityPub(
'http://fake-ghost-activitypub.test/.ghost/activitypub/outbox/index',
{
headers: {
Accept: 'application/ld+json',
},
},
);
const initialResponseJson = await initialResponse.json();
const firstPageReponse = await fetchActivityPub(initialResponseJson.first, {
headers: {
Accept: 'application/ld+json',
},
});
const outbox = await firstPageReponse.json();
return (outbox.orderedItems || []).find((item) => item.id === activity.id);
export async function findInOutbox(activity) {
const initialResponse = await fetchActivityPub(
'http://fake-ghost-activitypub.test/.ghost/activitypub/outbox/index',
{
headers: {
Accept: 'application/ld+json',
},
},
);
const initialResponseJson = await initialResponse.json();
const firstPageResponse = await fetchActivityPub(initialResponseJson.first, {
headers: {
Accept: 'application/ld+json',
},
});
if (!firstPageResponse.ok) {
throw new Error(`Outbox request failed: ${firstPageResponse.status}`);
}
const outbox = await firstPageResponse.json();
return (outbox.orderedItems || []).find((item) => item.id === activity.id);
}

Comment on lines +58 to +66
When('we undo the repost of the object {string}', async function (name) {
const id = this.objects[name].id;
this.response = await fetchActivityPub(
`http://fake-ghost-activitypub.test/.ghost/activitypub/actions/derepost/${encodeURIComponent(id)}`,
{
method: 'POST',
},
);
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Same missing assertion for the undo-repost endpoint

Mirror the check suggested above to ensure failures surface.

Comment on lines +10 to +17
const id = this.objects[name].id;
this.response = await fetchActivityPub(
`http://fake-ghost-activitypub.test/.ghost/activitypub/actions/repost/${encodeURIComponent(id)}`,
{
method: 'POST',
},
);
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Assert the POST outcome

The test records this.response but never asserts it. A 500 response would silently pass.

-    this.response = await fetchActivityPub( … );
+    this.response = await fetchActivityPub( … );
+    assert(
+        this.response.ok,
+        `Repost request failed: ${this.response.status} ${this.response.statusText}`,
+    );
📝 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 id = this.objects[name].id;
this.response = await fetchActivityPub(
`http://fake-ghost-activitypub.test/.ghost/activitypub/actions/repost/${encodeURIComponent(id)}`,
{
method: 'POST',
},
);
});
const id = this.objects[name].id;
this.response = await fetchActivityPub(
`http://fake-ghost-activitypub.test/.ghost/activitypub/actions/repost/${encodeURIComponent(id)}`,
{
method: 'POST',
},
);
assert(
this.response.ok,
`Repost request failed: ${this.response.status} ${this.response.statusText}`,
);
});

Comment on lines +247 to +263
const initialResponse = await fetchActivityPub(
'http://fake-ghost-activitypub.test/.ghost/activitypub/outbox/index',
{
headers: {
Accept: 'application/ld+json',
},
},
);
const initialResponseJson = await initialResponse.json();
const firstPageReponse = await fetchActivityPub(initialResponseJson.first, {
headers: {
Accept: 'application/ld+json',
},
});
const outbox = await firstPageReponse.json();

const found = (outbox.orderedItems || []).find((item) => {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Handle HTTP errors and fix typo

Same concerns as earlier helpers:

  1. firstPageReponse typo.
  2. Missing response.ok checks.
-    const firstPageReponse = await fetchActivityPub(initialResponseJson.first, {
+    const firstPageResponse = await fetchActivityPub(initialResponseJson.first, {
         headers: { Accept: 'application/ld+json' },
     });
-    const outbox = await firstPageReponse.json();
+    if (!firstPageResponse.ok) {
+        throw new Error(
+            `Outbox page fetch failed: ${firstPageResponse.status}`,
+        );
+    }
+    const outbox = await firstPageResponse.json();

Committable suggestion skipped: line range outside the PR's diff.

@mike182uk mike182uk merged commit 38f0378 into main May 6, 2025
7 checks passed
@mike182uk mike182uk deleted the cucumber-split-up-steps branch May 6, 2025 07:42
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.

3 participants