Skip to content

Commit

Permalink
Outbox: Basic rescheduling (#1223)
Browse files Browse the repository at this point in the history
* Outbox: Basic rescheduling

* add tests

* Add tests

* reuse ActivityPub_Outbox_TestCase class

* fix phpcs issues

* update tests

* fix tests

* fix tests

* debug

* debug

* types?

* add missing hook

props @obenland

* simpified code

props @obenland

* remove unused namespace definitions

---------

Co-authored-by: Konstantin Obenland <[email protected]>
  • Loading branch information
pfefferle and obenland authored Jan 28, 2025
1 parent 6041035 commit 65e7d32
Show file tree
Hide file tree
Showing 2 changed files with 216 additions and 0 deletions.
25 changes: 25 additions & 0 deletions includes/class-scheduler.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Activitypub\Scheduler\Post;
use Activitypub\Scheduler\Actor;
use Activitypub\Scheduler\Comment;
use Activitypub\Collection\Outbox;
use Activitypub\Collection\Followers;

/**
Expand All @@ -29,6 +30,8 @@ public static function init() {
\add_action( 'activitypub_update_followers', array( self::class, 'update_followers' ) );
\add_action( 'activitypub_cleanup_followers', array( self::class, 'cleanup_followers' ) );

\add_action( 'activitypub_reprocess_outbox', array( self::class, 'reprocess_outbox' ) );

\add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_outbox_activity_for_federation' ) );
}

Expand Down Expand Up @@ -59,6 +62,10 @@ public static function register_schedules() {
if ( ! \wp_next_scheduled( 'activitypub_cleanup_followers' ) ) {
\wp_schedule_event( time(), 'daily', 'activitypub_cleanup_followers' );
}

if ( ! \wp_next_scheduled( 'activitypub_reprocess_outbox' ) ) {
\wp_schedule_event( time(), 'hourly', 'activitypub_reprocess_outbox' );
}
}

/**
Expand Down Expand Up @@ -158,4 +165,22 @@ public static function schedule_outbox_activity_for_federation( $id ) {
);
}
}

/**
* Reprocess the outbox.
*/
public static function reprocess_outbox() {
$ids = \get_posts(
array(
'post_type' => Outbox::POST_TYPE,
'post_status' => 'pending',
'posts_per_page' => 10,
'fields' => 'ids',
)
);

foreach ( $ids as $id ) {
self::schedule_outbox_activity_for_federation( $id );
}
}
}
191 changes: 191 additions & 0 deletions tests/includes/class-test-scheduler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<?php
/**
* Test file for Scheduler class.
*
* @package ActivityPub
*/

namespace Activitypub\Tests;

use Activitypub\Scheduler;
use Activitypub\Collection\Outbox;
use Activitypub\Activity\Base_Object;
use WP_UnitTestCase;

/**
* Test class for Scheduler.
*
* @coversDefaultClass \Activitypub\Scheduler
*/
class Test_Scheduler extends WP_UnitTestCase {
/**
* Test user ID.
*
* @var int
*/
protected static $user_id;

/**
* Create fake data before tests run.
*
* @param WP_UnitTest_Factory $factory Helper that creates fake data.
*/
public static function wpSetUpBeforeClass( $factory ) {
self::$user_id = $factory->user->create(
array(
'role' => 'author',
)
);
}

/**
* Clean up after tests.
*/
public static function wpTearDownAfterClass() {
wp_delete_user( self::$user_id );
}

/**
* Test reprocess_outbox method.
*
* @covers ::reprocess_outbox
*/
public function test_reprocess_outbox() {
// Create test activity objects.
$activity_object = new Base_Object();
$activity_object->set_content( 'Test Content' );
$activity_object->set_type( 'Note' );
$activity_object->set_id( 'https://example.com/test-id' );

// Add multiple pending activities.
$pending_ids = array();
for ( $i = 0; $i < 3; $i++ ) {
$pending_ids[] = Outbox::add(
$activity_object,
'Create',
self::$user_id,
ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC
);
}

// Track scheduled events.
$scheduled_events = array();
add_filter(
'schedule_event',
function ( $event ) use ( &$scheduled_events ) {
if ( 'activitypub_process_outbox' === $event->hook ) {
$scheduled_events[] = $event->args[0];
}
return $event;
}
);

// Run reprocess_outbox.
Scheduler::reprocess_outbox();

// Verify each pending activity was scheduled.
$this->assertCount( 3, $scheduled_events, 'Should schedule 3 activities for processing' );
foreach ( $pending_ids as $id ) {
$this->assertContains( $id, $scheduled_events, "Activity $id should be scheduled" );
}

// Test with published activities (should not be scheduled).
$published_id = Outbox::add(
$activity_object,
'Create',
self::$user_id,
ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC
);
wp_update_post(
array(
'ID' => $published_id,
'post_status' => 'publish',
)
);

// Reset tracked events.
$scheduled_events = array();

// Run reprocess_outbox again.
Scheduler::reprocess_outbox();

// Verify published activity was not scheduled.
$this->assertNotContains( $published_id, $scheduled_events, 'Published activity should not be scheduled' );

// Clean up.
foreach ( $pending_ids as $id ) {
wp_delete_post( $id, true );
}
wp_delete_post( $published_id, true );
remove_all_filters( 'schedule_event' );
}

/**
* Test reprocess_outbox with no pending activities.
*
* @covers ::reprocess_outbox
*/
public function test_reprocess_outbox_no_pending() {
$scheduled_events = array();
add_filter(
'schedule_event',
function ( $event ) use ( &$scheduled_events ) {
if ( 'activitypub_process_outbox' === $event->hook ) {
$scheduled_events[] = $event->args[0];
}
return $event;
}
);

// Run reprocess_outbox with no pending activities.
Scheduler::reprocess_outbox();

// Verify no events were scheduled.
$this->assertEmpty( $scheduled_events, 'No events should be scheduled when there are no pending activities' );

remove_all_filters( 'schedule_event' );
}

/**
* Test reprocess_outbox scheduling behavior.
*
* @covers ::reprocess_outbox
*/
public function test_reprocess_outbox_scheduling() {
// Create a test activity.
$activity_object = new Base_Object();
$activity_object->set_content( 'Test Content' );
$activity_object->set_type( 'Note' );
$activity_object->set_id( 'https://example.com/test-id-2' );

$pending_id = Outbox::add(
$activity_object,
'Create',
self::$user_id,
ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC
);

// Track scheduled events and their timing.
$scheduled_time = 0;
add_filter(
'schedule_event',
function ( $event ) use ( &$scheduled_time ) {
if ( 'activitypub_process_outbox' === $event->hook ) {
$scheduled_time = $event->timestamp;
}
return $event;
}
);

// Run reprocess_outbox.
Scheduler::reprocess_outbox();

// Verify scheduling time.
$this->assertGreaterThan( 0, $scheduled_time, 'Event should be scheduled with a future timestamp' );
$this->assertGreaterThanOrEqual( time() + 10, $scheduled_time, 'Event should be scheduled at least 10 seconds in the future' );

// Clean up.
wp_delete_post( $pending_id, true );
remove_all_filters( 'schedule_event' );
}
}

0 comments on commit 65e7d32

Please sign in to comment.