From e895687a76577b07f8cbb457c1830f91e823bbde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Wed, 25 Dec 2024 14:21:25 +0100 Subject: [PATCH] Add tests for incoming delete --- composer.json | 2 +- includes/activitypub/class-handler.php | 1 - includes/activitypub/handler/class-create.php | 1 + includes/activitypub/handler/class-delete.php | 12 +- includes/activitypub/handler/class-update.php | 2 + .../activitypub/transmogrifier/class-base.php | 23 ++-- .../transmogrifier/class-gatherpress.php | 2 +- .../class-the-events-calendar.php | 24 ++-- .../transmogrifier/class-vs-event-list.php | 2 +- .../transmogrifier/class-test-gatherpress.php | 85 +++++++++++++ .../class-test-the-events-calendar.php | 117 ++++++++++++++---- .../class-test-vs-event-list.php | 31 ++++- 12 files changed, 248 insertions(+), 54 deletions(-) diff --git a/composer.json b/composer.json index 2c96db8..09cd0ed 100644 --- a/composer.json +++ b/composer.json @@ -61,7 +61,7 @@ ], "test-debug": [ "@prepare-test", - "@test-vs-event-list" + "@test-the-events-calendar" ], "test-vs-event-list": "phpunit --filter=vs_event_list", "test-the-events-calendar": "phpunit --filter=the_events_calendar", diff --git a/includes/activitypub/class-handler.php b/includes/activitypub/class-handler.php index df65916..0fa1433 100644 --- a/includes/activitypub/class-handler.php +++ b/includes/activitypub/class-handler.php @@ -12,7 +12,6 @@ namespace Event_Bridge_For_ActivityPub\ActivityPub; // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore -use Event_Bridge_For_ActivityPub\Event_Sources; use Event_Bridge_For_ActivityPub\ActivityPub\Handler\Accept; use Event_Bridge_For_ActivityPub\ActivityPub\Handler\Update; use Event_Bridge_For_ActivityPub\ActivityPub\Handler\Create; diff --git a/includes/activitypub/handler/class-create.php b/includes/activitypub/handler/class-create.php index 5d22847..eb1a018 100644 --- a/includes/activitypub/handler/class-create.php +++ b/includes/activitypub/handler/class-create.php @@ -41,6 +41,7 @@ class Create { return; } + // Check that we are actually following this actor. if ( ! Event_Sources::actor_is_event_source( $activity['actor'] ) ) { return false; } diff --git a/includes/activitypub/handler/class-delete.php b/includes/activitypub/handler/class-delete.php index a435860..6a55462 100644 --- a/includes/activitypub/handler/class-delete.php +++ b/includes/activitypub/handler/class-delete.php @@ -11,6 +11,8 @@ use Activitypub\Collection\Actors; use Event_Bridge_For_ActivityPub\Event_Sources; use Event_Bridge_For_ActivityPub\Setup; +use function Activitypub\object_to_uri; + /** * Handle Delete requests. */ @@ -39,14 +41,12 @@ class Delete { return; } + // Check that we are actually following this actor. if ( ! Event_Sources::actor_is_event_source( $activity['actor'] ) ) { - return; + return false; } - // Check if an object is set. - if ( ! isset( $activity['object']['type'] ) || 'Event' !== $activity['object']['type'] ) { - return; - } + $id = object_to_uri( $activity['object'] ); $transmogrifier = Setup::get_transmogrifier(); @@ -54,6 +54,6 @@ class Delete { return; } - $transmogrifier->delete( $activity['object'] ); + $transmogrifier->delete( $id ); } } diff --git a/includes/activitypub/handler/class-update.php b/includes/activitypub/handler/class-update.php index ac71f7e..0ebc6c3 100644 --- a/includes/activitypub/handler/class-update.php +++ b/includes/activitypub/handler/class-update.php @@ -41,6 +41,8 @@ class Update { return; } + // Check that we are actually following this actor. + if ( ! Event_Sources::actor_is_event_source( $activity['actor'] ) ) { return; } diff --git a/includes/activitypub/transmogrifier/class-base.php b/includes/activitypub/transmogrifier/class-base.php index 7833135..c67bcfc 100644 --- a/includes/activitypub/transmogrifier/class-base.php +++ b/includes/activitypub/transmogrifier/class-base.php @@ -62,14 +62,17 @@ abstract class Base { } /** - * Get post. + * Get WordPress post by ActivityPub object ID. + * + * @param int $activitypub_id The ActivityPub object ID. + * @return int The WordPress Post ID. */ - protected function get_post_id_from_activitypub_id() { + protected static function get_post_id_from_activitypub_id( $activitypub_id ) { global $wpdb; return $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE guid=%s", - esc_sql( $this->activitypub_event->get_id() ) + esc_sql( $activitypub_id ), ) ); } @@ -273,18 +276,10 @@ abstract class Base { /** * Delete a local event in WordPress that is a cached remote one. * - * @param array $activitypub_event The ActivityPub event as associative array. + * @param int $activitypub_event_id The ActivityPub events ID. */ - public function delete( $activitypub_event ) { - $activitypub_event = Event::init_from_array( $activitypub_event ); - - if ( is_wp_error( $activitypub_event ) ) { - return; - } - - $this->activitypub_event = $activitypub_event; - - $post_id = $this->get_post_id_from_activitypub_id(); + public function delete( $activitypub_event_id ) { + $post_id = self::get_post_id_from_activitypub_id( $activitypub_event_id); if ( ! $post_id ) { return new WP_Error( diff --git a/includes/activitypub/transmogrifier/class-gatherpress.php b/includes/activitypub/transmogrifier/class-gatherpress.php index 7da7305..3e374e1 100644 --- a/includes/activitypub/transmogrifier/class-gatherpress.php +++ b/includes/activitypub/transmogrifier/class-gatherpress.php @@ -127,7 +127,7 @@ class GatherPress extends Base { // Limit this as a safety measure. add_filter( 'wp_revisions_to_keep', array( self::class, 'revisions_to_keep' ) ); - $post_id = $this->get_post_id_from_activitypub_id(); + $post_id = self::get_post_id_from_activitypub_id( $this->activitypub_event->get_id() ); $args = array( 'post_title' => sanitize_text_field( $this->activitypub_event->get_name() ), diff --git a/includes/activitypub/transmogrifier/class-the-events-calendar.php b/includes/activitypub/transmogrifier/class-the-events-calendar.php index 6205a18..399e1b7 100644 --- a/includes/activitypub/transmogrifier/class-the-events-calendar.php +++ b/includes/activitypub/transmogrifier/class-the-events-calendar.php @@ -85,20 +85,27 @@ class The_Events_Calendar extends Base { return; } - $post_ids = tribe_events()->search( $location['name'] )->all(); + $post_ids = tribe_venues()->search( $location['name'] )->all(); $post_id = false; if ( count( $post_ids ) ) { $post_id = reset( $post_ids ); + if ( $post_id instanceof \WP_Post ) { + $post_id = $post_id->ID; + } } if ( $post_id && get_post_meta( $post_id, '_event_bridge_for_activitypub_is_remote_cached', true ) ) { - tribe_venues()->where( 'id', $post_id )->set_args( $this->get_venue_args( $location ) )->save()[0]; + $result = tribe_venues()->where( 'id', $post_id )->set_args( $this->get_venue_args( $location ) )->save(); + if ( array_key_exists( $post_id, $result ) && $result[ $post_id ] ) { + return $post_id; + } } else { $post = tribe_venues()->set_args( $this->get_venue_args( $location ) )->create(); if ( $post ) { $post_id = $post->ID; + update_post_meta( $post_id, '_event_bridge_for_activitypub_is_remote_cached', true ); } } @@ -167,7 +174,7 @@ class The_Events_Calendar extends Base { // Limit this as a safety measure. add_filter( 'wp_revisions_to_keep', array( self::class, 'revisions_to_keep' ) ); - $post_id = $this->get_post_id_from_activitypub_id(); + $post_id = self::get_post_id_from_activitypub_id( $this->activitypub_event->get_id() ); $duration = $this->get_duration(); @@ -199,20 +206,23 @@ class The_Events_Calendar extends Base { if ( $post_id ) { $args['post_title'] = $args['title']; $args['post_content'] = $args['content']; - // Update existing GatherPress event post. - $post = \Tribe__Events__API::updateEvent( $post_id, $args ); + // Update existing The Events Calendar event post. + $post_id = \Tribe__Events__API::updateEvent( $post_id, $args ); } else { $post = $tribe_event->set_args( $args )->create(); + if ( $post instanceof \WP_Post ) { + $post_id = $post->ID; + } } - if ( ! $post ) { + if ( ! $post_id || is_wp_error( $post_id ) ) { return false; } // Limit this as a safety measure. remove_filter( 'wp_revisions_to_keep', array( self::class, 'revisions_to_keep' ) ); - return $post->ID; + return $post_id; } /** diff --git a/includes/activitypub/transmogrifier/class-vs-event-list.php b/includes/activitypub/transmogrifier/class-vs-event-list.php index 01fb36a..260a93e 100644 --- a/includes/activitypub/transmogrifier/class-vs-event-list.php +++ b/includes/activitypub/transmogrifier/class-vs-event-list.php @@ -101,7 +101,7 @@ class VS_Event_List extends Base { // Limit this as a safety measure. \add_filter( 'wp_revisions_to_keep', array( self::class, 'revisions_to_keep' ) ); - $post_id = $this->get_post_id_from_activitypub_id(); + $post_id = self::get_post_id_from_activitypub_id( $this->activitypub_event->get_id() ); $args = array( 'post_title' => \sanitize_text_field( $this->activitypub_event->get_name() ), diff --git a/tests/includes/activitypub/transmogrifier/class-test-gatherpress.php b/tests/includes/activitypub/transmogrifier/class-test-gatherpress.php index 8c81375..1c7d783 100644 --- a/tests/includes/activitypub/transmogrifier/class-test-gatherpress.php +++ b/tests/includes/activitypub/transmogrifier/class-test-gatherpress.php @@ -147,4 +147,89 @@ class Test_GatherPress extends \WP_UnitTestCase { $this->assertEquals( $json['object']['location']['name'], $event->get_venue_information()['name'] ); $this->assertEquals( false, $event->get_venue_information()['is_online_event'] ); } + + /** + * Test receiving event from followed actor. + */ + public function test_incoming_event_update_and_delete() { + \add_filter( 'activitypub_defer_signature_verification', '__return_true' ); + + $json = array( + 'id' => 'https://remote.example/@organizer/events/new-year-party#create', + 'type' => 'Create', + 'actor' => 'https://remote.example/@organizer', + 'object' => array( + 'id' => 'https://remote.example/@organizer/events/new-year-party', + 'type' => 'Event', + 'startTime' => \gmdate( 'Y-m-d\TH:i:s\Z', time() + WEEK_IN_SECONDS ), + 'endTime' => \gmdate( 'Y-m-d\TH:i:s\Z', time() + WEEK_IN_SECONDS + HOUR_IN_SECONDS ), + 'name' => 'Fediverse Party', + 'to' => 'https://www.w3.org/ns/activitystreams#Public', + 'published' => \gmdate( 'Y-m-d\TH:i:s\Z', time() ), + 'location' => array( + 'type' => 'Place', + 'name' => 'Fediverse Concert Hall', + 'address' => 'Fedistreet 13, Feditown 1337', + ), + ), + ); + + $request = new WP_REST_Request( 'POST', '/activitypub/1.0/users/0/inbox' ); + $request->set_header( 'Content-Type', 'application/activity+json' ); + $request->set_body( \wp_json_encode( $json ) ); + + // Dispatch the request. + $response = \rest_do_request( $request ); + $this->assertEquals( 202, $response->get_status() ); + + // Check if post has been created. + $event_query = Event_Query::get_instance(); + $the_query = $event_query->get_upcoming_events(); + + $this->assertEquals( true, $the_query->have_posts() ); + $this->assertEquals( 1, $the_query->post_count ); + + // Initialize new GatherPress Event object. + $event = new Event( $the_query->get_posts()[0] ); + + $this->assertEquals( $json['object']['name'], $event->event->post_title ); + $this->assertEquals( $json['object']['startTime'], $event->get_datetime_start( 'Y-m-d\TH:i:s\Z' ) ); + $this->assertEquals( $json['object']['endTime'], $event->get_datetime_end( 'Y-m-d\TH:i:s\Z' ) ); + $this->assertEquals( $json['object']['location']['address'], $event->get_venue_information()['full_address'] ); + $this->assertEquals( $json['object']['location']['name'], $event->get_venue_information()['name'] ); + $this->assertEquals( false, $event->get_venue_information()['is_online_event'] ); + + // Now we receive an update of that event. + $json['type'] = 'Update'; + $json['object']['name'] = 'Updated name'; + $json['object']['location']['address'] = 'Updated address'; + + $request->set_body( \wp_json_encode( $json ) ); + $response = \rest_do_request( $request ); + + // We do not except duplicated. + $the_query = $event_query->get_upcoming_events(); + + $this->assertEquals( true, $the_query->have_posts() ); + $this->assertEquals( 1, $the_query->post_count ); + + // Check the updated representation of the event within The Events Calendar. + $event = new Event( $the_query->get_posts()[0] ); + + $this->assertEquals( 'Updated name', $event->event->post_title ); + $this->assertEquals( 'Updated address', $event->get_venue_information()['name'] ); + + // Test delete. + $json['type'] = 'Delete'; + $json['object'] = $json['object']['id']; + $request->set_body( \wp_json_encode( $json ) ); + $response = \rest_do_request( $request ); + $this->assertEquals( 202, $response->get_status() ); + + // We do expect the event to be removed. + $the_query = $event_query->get_upcoming_events(); + $this->assertFalse( $the_query->have_posts() ); + + \remove_filter( 'activitypub_defer_signature_verification', '__return_true' ); + } } diff --git a/tests/includes/activitypub/transmogrifier/class-test-the-events-calendar.php b/tests/includes/activitypub/transmogrifier/class-test-the-events-calendar.php index 2bd29b3..f1ef262 100644 --- a/tests/includes/activitypub/transmogrifier/class-test-the-events-calendar.php +++ b/tests/includes/activitypub/transmogrifier/class-test-the-events-calendar.php @@ -1,6 +1,6 @@ venues instanceof \Tribe\Events\Collections\Lazy_Post_Collection ) { + return $event->venues->first(); + } elseif ( empty( $event->venues ) || ! empty( $event->venues[0] ) ) { + return null; + } else { + return $event->venues[0]; + } + } + /** * Test receiving event from followed actor. */ @@ -115,22 +132,14 @@ class Test_The_Events_Calendar extends \WP_UnitTestCase { $this->assertEquals( 1, count( $events ) ); - // Initialize new GatherPress Event object. + // Initialize new Tribe Event object. $event = tribe_get_event( $events[0] ); $this->assertEquals( $json['object']['name'], $event->post_title ); $this->assertEquals( $json['object']['startTime'], $event->dates->start->format( 'Y-m-d\TH:i:s\Z' ) ); $this->assertEquals( $json['object']['endTime'], $event->dates->end->format( 'Y-m-d\TH:i:s\Z' ) ); - $venues = $event->venues; - // Get first venue. We currently only support a single venue. - if ( $venues instanceof \Tribe\Events\Collections\Lazy_Post_Collection ) { - $venue = $venues->first(); - } elseif ( empty( $this->wp_object->venues ) || ! empty( $this->wp_object->venues[0] ) ) { - return null; - } else { - $venue = $venues[0]; - } + $venue = self::get_first_tribe_venue_of_tribe_event( $event ); $this->assertEquals( $json['object']['location']['address'], $venue->address ); $this->assertEquals( $json['object']['location']['name'], $venue->post_title ); @@ -185,22 +194,14 @@ class Test_The_Events_Calendar extends \WP_UnitTestCase { $this->assertEquals( 1, count( $events ) ); - // Initialize new GatherPress Event object. + // Initialize new Tribe Event object. $event = tribe_get_event( $events[0] ); $this->assertEquals( $json['object']['name'], $event->post_title ); $this->assertEquals( $json['object']['startTime'], $event->dates->start->format( 'Y-m-d\TH:i:s\Z' ) ); $this->assertEquals( $json['object']['endTime'], $event->dates->end->format( 'Y-m-d\TH:i:s\Z' ) ); - $venues = $event->venues; - // Get first venue. We currently only support a single venue. - if ( $venues instanceof \Tribe\Events\Collections\Lazy_Post_Collection ) { - $venue = $venues->first(); - } elseif ( empty( $this->wp_object->venues ) || ! empty( $this->wp_object->venues[0] ) ) { - return null; - } else { - $venue = $venues[0]; - } + $venue = self::get_first_tribe_venue_of_tribe_event( $event ); $this->assertEquals( $json['object']['location']['name'], $venue->post_title ); $this->assertEquals( $json['object']['location']['address']['streetAddress'], $venue->address ); @@ -212,4 +213,76 @@ class Test_The_Events_Calendar extends \WP_UnitTestCase { \remove_filter( 'activitypub_defer_signature_verification', '__return_true' ); } + + /** + * Test handling updates and deletes. + */ + public function test_incoming_event_updates() { + \add_filter( 'activitypub_defer_signature_verification', '__return_true' ); + + $json = array( + 'id' => 'https://remote.example/@organizer/events/new-year-party#create', + 'type' => 'Create', + 'actor' => 'https://remote.example/@organizer', + 'object' => array( + 'id' => 'https://remote.example/@organizer/events/new-year-party', + 'type' => 'Event', + 'startTime' => \gmdate( 'Y-m-d\TH:i:s\Z', time() + WEEK_IN_SECONDS ), + 'endTime' => \gmdate( 'Y-m-d\TH:i:s\Z', time() + WEEK_IN_SECONDS + HOUR_IN_SECONDS ), + 'name' => 'Fediverse Party for The Events Calendar', + 'to' => 'https://www.w3.org/ns/activitystreams#Public', + 'published' => \gmdate( 'Y-m-d\TH:i:s\Z', time() ), + 'location' => array( + 'type' => 'Place', + 'name' => 'Fediverse Concert Hall', + 'address' => 'Fedistreet 13, Feditown 1337', + ), + ), + ); + + $request = new WP_REST_Request( 'POST', '/activitypub/1.0/users/0/inbox' ); + $request->set_header( 'Content-Type', 'application/activity+json' ); + $request->set_body( \wp_json_encode( $json ) ); + + // Dispatch the request. + $response = \rest_do_request( $request ); + $this->assertEquals( 202, $response->get_status() ); + + // Check if post has been created. + $events = tribe_get_events(); + + $this->assertEquals( 1, count( $events ) ); + + // Now we receive an update of that event. + $json['type'] = 'Update'; + $json['object']['name'] = 'Updated name'; + $json['object']['location']['address'] = 'Updated address'; + + $request->set_body( \wp_json_encode( $json ) ); + $response = \rest_do_request( $request ); + + // We do not except duplicated. + $events = tribe_get_events(); + $this->assertEquals( 1, count( $events ) ); + + // Check the updated representation of the event within The Events Calendar. + $event = tribe_get_event( $events[0] ); + $venue = self::get_first_tribe_venue_of_tribe_event( $event ); + + $this->assertEquals( 'Updated name', $event->post_title ); + $this->assertEquals( 'Updated address', $venue->address ); + + // Test delete. + $json['type'] = 'Delete'; + $json['object'] = $json['object']['id']; + $request->set_body( \wp_json_encode( $json ) ); + $response = \rest_do_request( $request ); + $this->assertEquals( 202, $response->get_status() ); + + // We do expect the event to be removed. + $events = tribe_get_events(); + $this->assertEmpty( $events ); + + \remove_filter( 'activitypub_defer_signature_verification', '__return_true' ); + } } diff --git a/tests/includes/activitypub/transmogrifier/class-test-vs-event-list.php b/tests/includes/activitypub/transmogrifier/class-test-vs-event-list.php index 5c74172..fe8ae56 100644 --- a/tests/includes/activitypub/transmogrifier/class-test-vs-event-list.php +++ b/tests/includes/activitypub/transmogrifier/class-test-vs-event-list.php @@ -114,7 +114,6 @@ class Test_VS_Event_List extends \WP_UnitTestCase { $this->assertEquals( 202, $response->get_status() ); $events = get_posts( array( 'post_type' => IntegrationsVS_Event_List::get_post_type() ) ); - $this->assertCount( 1, $events ); $event = $events[0]; @@ -122,6 +121,36 @@ class Test_VS_Event_List extends \WP_UnitTestCase { $this->assertEquals( $json['object']['startTime'], \gmdate( 'Y-m-d\TH:i:s\Z', get_post_meta( $event->ID, 'event-start-date', true ) ) ); $this->assertEquals( $json['object']['endTime'], \gmdate( 'Y-m-d\TH:i:s\Z', get_post_meta( $event->ID, 'event-date', true ) ) ); $this->assertStringStartsWith( $json['object']['location']['name'], get_post_meta( $event->ID, 'event-location', true ) ); + $this->assertStringContainsString( $json['object']['location']['address'], get_post_meta( $event->ID, 'event-location', true ) ); + + // Now we receive an update of that event. + $json['type'] = 'Update'; + $json['object']['name'] = 'Updated name'; + $json['object']['location']['address'] = 'Updated address'; + + $request->set_body( \wp_json_encode( $json ) ); + $response = \rest_do_request( $request ); + + // We do not except duplicated. + $events = get_posts( array( 'post_type' => IntegrationsVS_Event_List::get_post_type() ) ); + $this->assertCount( 1, $events ); + $event = $events[0]; + $this->assertStringContainsString( 'Updated address' , get_post_meta( $event->ID, 'event-location', true ) ); + + // We should see the updates. + $this->assertEquals( 'Updated name', $event->post_title ); + + // Test delete. + $json['type'] = 'Delete'; + $json['object'] = $json['object']['id']; + $request->set_body( \wp_json_encode( $json ) ); + $response = \rest_do_request( $request ); + $this->assertEquals( 202, $response->get_status() ); + + // We do expect the event to be removed. + $events = get_posts( array( 'post_type' => IntegrationsVS_Event_List::get_post_type() ) ); + $this->assertCount( 0, $events ); + \remove_filter( 'activitypub_defer_signature_verification', '__return_true' ); } }