diff --git a/.forgejo/workflows/phpunit.yml b/.forgejo/workflows/phpunit.yml index 7cd24a9..23321d9 100644 --- a/.forgejo/workflows/phpunit.yml +++ b/.forgejo/workflows/phpunit.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - improve_tests pull_request: env: @@ -36,7 +37,7 @@ jobs: path: | ${{ env.WP_CORE_DIR }} ${{ env.WP_TESTS_DIR }} - key: cache-wordpress-1 + key: cache-wordpress-3 - name: Cache Composer id: cache-composer-phpunit @@ -67,11 +68,11 @@ jobs: - name: Setup Test Environment if: steps.cache-wordpress.outputs.cache-hit != 'true' - run: bash tests/install-wp-tests.sh wordpress_test root root 127.0.0.1 6.6 false false false false + run: bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1 6.6 false false false false - name: Initialize WordPress test database if: steps.cache-wordpress.outputs.cache-hit != 'false' - run: bash tests/install-wp-tests.sh wordpress_test root root 127.0.0.1 6.6 false true true true + run: bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1 6.6 false true true true - name: Run PHPUnit run: cd /workspace/Event-Federation/wordpress-activitypub-event-extensions/ && ./vendor/bin/phpunit diff --git a/Dockerfile b/Dockerfile index 56e97a7..d2203ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,18 @@ RUN apk update \ RUN docker-php-ext-install mysqli -# Install Composer +# Install Xdebug +RUN apk add --no-cache $PHPIZE_DEPS \ + && apk add --update linux-headers \ + && pecl install xdebug \ + && docker-php-ext-enable xdebug \ + && apk del --purge $PHPIZE_DEPS \ + && echo "xdebug.start_with_request=yes" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \ + && echo "xdebug.client_host=host.docker.internal" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \ + && echo "xdebug.mode=debug" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \ + && echo "xdebug.idekey=VSCODE" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini + +# Install Composer RUN EXPECTED_CHECKSUM=$(curl -s https://composer.github.io/installer.sig) \ && curl https://getcomposer.org/installer -o composer-setup.php \ && ACTUAL_CHECKSUM="$(php -r "echo hash_file('sha384', 'composer-setup.php');")" \ diff --git a/tests/install-wp-tests.sh b/bin/install-wp-tests.sh similarity index 84% rename from tests/install-wp-tests.sh rename to bin/install-wp-tests.sh index 286ea40..ff43d3a 100755 --- a/tests/install-wp-tests.sh +++ b/bin/install-wp-tests.sh @@ -15,6 +15,16 @@ SKIP_WP_INSTALL=${7-false} SKIP_PLUGINS_INSTALL=${8-false} SKIP_TEST_SUITE_INSTALL=${9-false} +# Initialize the plugin list +PLUGINS="" + +# Parse optional --plugins argument +while [[ "$#" -gt 0 ]]; do + case $1 in + --plugins) PLUGINS="$2"; shift ;; + esac + shift +done TMPDIR=${TMPDIR-/tmp} TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") @@ -182,23 +192,50 @@ install_db() { if [ $(mysql --user="$DB_USER" --password="$DB_PASS"$EXTRA --execute='show databases;' | grep ^$DB_NAME$) ] then echo "Reinstalling will delete the existing test database ($DB_NAME)" - read -p 'Are you sure you want to proceed? [y/N]: ' DELETE_EXISTING_DB - recreate_db $DELETE_EXISTING_DB + recreate_db yes else create_db fi } +install_wp_plugin() { + PLUGIN_NAME=$1 + + mkdir -p "$WP_CORE_DIR/wp-content/plugins/" + + if [ -d "$WP_CORE_DIR/wp-content/plugins/$PLUGIN_NAME" ]; then + return; + fi + + # Get the latest tag. + LATEST_TAG=$(svn log https://plugins.svn.wordpress.org/$PLUGIN_NAME/tags --limit 1 | awk 'NR == 4 { print $4 }') + PLUGIN_FILE="$PLUGIN_NAME.$LATEST_TAG.zip" + URL="https://downloads.wordpress.org/plugin/$PLUGIN_FILE" + + # Check if the plugin file already exists + if ! test -f "$TMPDIR/$PLUGIN_FILE"; then + download $URL "$TMPDIR/$PLUGIN_FILE" + fi + + # Unzip the plugin into the WordPress must-use plugins directory + unzip -q -o "$TMPDIR/$PLUGIN_FILE" -d "$WP_CORE_DIR/wp-content/plugins/" +} + install_wp_plugins() { if [ "$SKIP_PLUGINS_INSTALL" = "true" ]; then echo "Skipping WordPress plugin installation." return 0 fi - ACTIVITYPUB_FILE="activitypub.3.2.5.zip" - if ! test -f $TMPDIR/$ACTIVITYPUB_FILE; then - download https://downloads.wordpress.org/plugin/$ACTIVITYPUB_FILE $TMPDIR/$ACTIVITYPUB_FILE - fi - unzip -o $TMPDIR/$ACTIVITYPUB_FILE -d $WP_CORE_DIR/wp-content/plugins/ + # Always install the ActivityPub plugin. + install_wp_plugin activitypub + install_wp_plugin the-events-calendar + # Install additional plugins. + # if [[ -n "$PLUGINS" ]]; then + # IFS=',' read -ra PLUGIN_ARRAY <<< "$PLUGINS" + # for plugin in "${PLUGIN_ARRAY[@]}"; do + # install_wp_plugin "$plugin" + # done + # fi } install_wp diff --git a/composer.json b/composer.json index 2a2a7cc..ba5b52a 100644 --- a/composer.json +++ b/composer.json @@ -43,7 +43,7 @@ ], "test": [ "composer install", - "tests/install-wp-tests.sh wordpress-test root wordpress-test test-db latest true", + "bin/install-wp-tests.sh wordpress-test root wordpress-test test-db latest true", "phpunit" ] } diff --git a/docker-compose.yml b/docker-compose.yml index 9a4085b..47d5117 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,20 @@ version: '3' # Install docker and docker compose and than just run: # docker compose up +# To live debug in VSCode add this launch configuration to your .vscode/launch.json. +# It assumes that the WordPress root-dir is your workspace root. +# +# { +# "name": "Listen for PHPUnit", +# "type": "php", +# "request": "launch", +# "port": 9003, +# "pathMappings": { +# "/app/": "${workspaceRoot}/wp-content/plugins/activitypub-event-extensions/", +# "/tmp/wordpress/": "${workspaceRoot}/" +# }, +# }, + services: test-db: image: mariadb @@ -29,3 +43,5 @@ services: volumes: - .:/app command: ["composer", "run-script", "test"] + extra_hosts: + - "host.docker.internal:host-gateway" diff --git a/includes/activitypub/transformer/class-the-events-calendar.php b/includes/activitypub/transformer/class-the-events-calendar.php index e1b42fb..2c590b8 100644 --- a/includes/activitypub/transformer/class-the-events-calendar.php +++ b/includes/activitypub/transformer/class-the-events-calendar.php @@ -145,25 +145,52 @@ final class The_Events_Calendar extends Event { * @return Place|array The place/venue if one is set. */ public function get_location(): Place|null { - if ( empty( $this->wp_object->venues ) || ! empty( $this->wp_object->venues[0] ) ) { + // Get short handle for the venues. + $venues = $this->wp_object->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]; + } + + if ( ! $venue ) { return null; } - // We currently only support a single venue. - $event_venue = $this->wp_object->venues[0]; - $address = array( - 'addressCountry' => $event_venue->country, - 'addressLocality' => $event_venue->city, - 'addressRegion' => $event_venue->province, - 'postalCode' => $event_venue->zip, - 'streetAddress' => $event_venue->address, - 'type' => 'PostalAddress', - ); + // Set the address. + $address = array(); + + if ( ! empty( $venue->country ) ) { + $address['addressCountry'] = $venue->country; + } + + if ( ! empty( $venue->city ) ) { + $address['addressLocality'] = $venue->city; + } + + if ( ! empty( $venue->province ) ) { + $address['addressRegion'] = $venue->province; + } + + if ( ! empty( $venue->zip ) ) { + $address['postalCode'] = $venue->zip; + } + + if ( ! empty( $venue->address ) ) { + $address['streetAddress'] = $venue->address; + } + $address['type'] = 'PostalAddress'; $location = new Place(); - $location->set_address( $address ); - $location->set_id( $event_venue->permalink ); - $location->set_name( $event_venue->post_name ); + if ( count( $address ) > 1 ) { + $location->set_address( $address ); + } + $location->set_id( $venue->permalink ); + $location->set_name( $venue->post_title ); return $location; } diff --git a/includes/class-setup.php b/includes/class-setup.php index 2de709f..9e4a1e6 100644 --- a/includes/class-setup.php +++ b/includes/class-setup.php @@ -59,14 +59,15 @@ class Setup { * @since 1.0.0 */ protected function __construct() { - $this->activitypub_plugin_is_active = is_plugin_active( 'activitypub/activitypub.php' ); + $this->activitypub_plugin_is_active = defined( 'ACTIVITYPUB_PLUGIN_VERSION' ) || + is_plugin_active( 'activitypub/activitypub.php' ); // BeforeFirstRelease: decide whether we want to do anything at all when ActivityPub plugin is note active. // if ( ! $this->activitypub_plugin_is_active ) { // deactivate_plugins( ACTIVITYPUB_EVENT_EXTENSIONS_PLUGIN_FILE ); // return; // }. $this->active_event_plugins = self::detect_active_event_plugins(); - $this->activitypub_plugin_version = get_file_data( WP_PLUGIN_DIR . '/activitypub/activitypub.php', array( 'Version' ) )[0]; + $this->activitypub_plugin_version = self::get_activitypub_plugin_version(); $this->setup_hooks(); } @@ -95,6 +96,19 @@ class Setup { return self::$instance; } + /** + * LooksUp the current version of the ActivityPub. + * + * @return string The semantic Version. + */ + private static function get_activitypub_plugin_version(): string { + if ( defined( 'ACTIVITYPUB_PLUGIN_VERSION' ) ) { + return constant( 'ACTIVITYPUB_PLUGIN_VERSION' ); + } + $version = get_file_data( WP_PLUGIN_DIR . '/activitypub/activitypub.php', array( 'Version' ) )[0]; + return $version ?? '0.0.0'; + } + /** * Getter function for the active event plugins. * @@ -166,7 +180,7 @@ class Setup { ); // Check if the minimum required version of the ActivityPub plugin is installed. - if ( version_compare( $this->activitypub_plugin_version, ACTIVITYPUB_EVENT_EXTENSIONS_ACTIVITYPUB_PLUGIN_MIN_VERSION ) ) { + if ( ! version_compare( $this->activitypub_plugin_version, ACTIVITYPUB_EVENT_EXTENSIONS_ACTIVITYPUB_PLUGIN_MIN_VERSION ) ) { return; } @@ -206,7 +220,7 @@ class Setup { // The ActivityPub plugin is not active. add_action( 'admin_notices', array( 'Activitypub_Event_Extensions\Admin\General_Admin_Notices', 'activitypub_plugin_not_enabled' ), 10, 1 ); } - if ( version_compare( $this->activitypub_plugin_version, ACTIVITYPUB_EVENT_EXTENSIONS_ACTIVITYPUB_PLUGIN_MIN_VERSION ) ) { + if ( ! version_compare( $this->activitypub_plugin_version, ACTIVITYPUB_EVENT_EXTENSIONS_ACTIVITYPUB_PLUGIN_MIN_VERSION ) ) { // The ActivityPub plugin is too old. add_action( 'admin_notices', array( 'Activitypub_Event_Extensions\Admin\General_Admin_Notices', 'activitypub_plugin_version_too_old' ), 10, 1 ); } @@ -234,9 +248,9 @@ class Setup { // Get the transformer for a specific event plugins event-post type. foreach ( $this->active_event_plugins as $event_plugin ) { if ( $wp_object->post_type === $event_plugin->get_post_type() ) { - $transformer_class = $event_plugin->get_activitypub_event_transformer_class(); + $transformer_class = $event_plugin::get_activitypub_event_transformer_class(); if ( class_exists( $transformer_class ) ) { - return new $transformer_class( $wp_object, $event_plugin->get_event_category_taxonomy() ); + return new $transformer_class( $wp_object, $event_plugin::get_event_category_taxonomy() ); } } } @@ -256,8 +270,8 @@ class Setup { // If someone installs this plugin, we simply enable ActivityPub support for all currently active event post types. $activitypub_supported_post_types = get_option( 'activitypub_support_post_types', array() ); foreach ( $this->active_event_plugins as $event_plugin ) { - if ( ! in_array( $event_plugin['post_type'], $activitypub_supported_post_types, true ) ) { - $activitypub_supported_post_types[] = $event_plugin['post_type']; + if ( ! in_array( $event_plugin->get_post_type(), $activitypub_supported_post_types, true ) ) { + $activitypub_supported_post_types[] = $event_plugin->get_post_type(); } } update_option( 'activitypub_support_post_types', $activitypub_supported_post_types ); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 7d30eb9..e32baa7 100755 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -26,9 +26,21 @@ if ( ! file_exists( "{$_tests_dir}/includes/functions.php" ) ) { require_once "{$_tests_dir}/includes/functions.php"; /** - * Manually load the plugin being tested. + * Manually load the plugin being tested and its integrations. */ function _manually_load_plugin() { + $plugin_dir = ABSPATH . '/wp-content/plugins/'; + require_once $plugin_dir . 'activitypub/activitypub.php'; + $event_plugin = 'the-events-calendar'; + switch ( $event_plugin ) { + case 'the-events-calendar': + $plugin_file = 'the-events-calendar/the-events-calendar.php'; + require_once $plugin_dir . $plugin_file; + $current = get_option( 'active_plugins', array() ); + $current[] = $plugin_file; + sort( $current ); + update_option( 'active_plugins', $current ); + } require dirname( __DIR__ ) . '/activitypub-event-extensions.php'; } diff --git a/tests/test-class-plugin-the-events-calendar.php b/tests/test-class-plugin-the-events-calendar.php new file mode 100644 index 0000000..c2c9eb6 --- /dev/null +++ b/tests/test-class-plugin-the-events-calendar.php @@ -0,0 +1,182 @@ + array( + 'venue' => 'Minimal Venue', + 'status' => 'publish', + ), + 'complex_venue' => array( + 'venue' => 'Complex Venue', + 'status' => 'publish', + 'show_map' => false, + 'show_map_link' => false, + 'address' => 'Venue address', + 'city' => 'Venue city', + 'country' => 'Venue country', + 'province' => 'Venue province', + 'state' => 'Venue state', + 'stateprovince' => 'Venue stateprovince', + 'zip' => 'Venue zip', + 'phone' => 'Venue phone', + 'website' => 'http://venue.com', + ), + ); + + public const MOCKUP_EVENTS = array( + 'minimal_event' => array( + 'title' => 'My Event', + 'content' => 'Come to my event. Let\'s connect!', + 'start_date' => '+10 days 15:00:00', + 'duration' => HOUR_IN_SECONDS, + 'status' => 'publish', + ), + 'complex_event' => array( + 'title' => 'My Event', + 'content' => 'Come to my event. Let\'s connect!', + 'start_date' => '+10 days 15:00:00', + 'duration' => HOUR_IN_SECONDS, + 'status' => 'publish', + ), + ); + + /** + * Override the setup function, so that tests don't run if the Events Calendar is not active. + */ + public function set_up() { + parent::set_up(); + + if ( ! class_exists( '\Tribe__Events__Main' ) ) { + self::markTestSkipped( 'The Events Calendar plugin is not active.' ); + } + + // Make sure that ActivityPub support is enabled for The Events Calendar. + $aec = \Activitypub_Event_Extensions\Setup::get_instance(); + $aec->activate_activitypub_support_for_active_event_plugins(); + + // Delete all posts afterwards. + _delete_all_posts(); + } + + /** + * Test that the right transformer gets applied. + */ + public function test_transformer_class() { + // We only test for one event plugin being active at the same time, + // even though we support multiple onces in theory. + // But testing all combinations is beyond scope. + $active_event_plugins = \Activitypub_Event_Extensions\Setup::get_instance()->get_active_event_plugins(); + $this->assertEquals( 1, count( $active_event_plugins ) ); + + // Enable ActivityPub support for the event plugin. + $this->assertContains( 'tribe_events', get_option( 'activitypub_support_post_types' ) ); + + // Create a The Events Calendar Event without content. + $wp_object = tribe_events() + ->set_args( self::MOCKUP_EVENTS['minimal_event'] ) + ->create(); + + // Call the transformer Factory. + $transformer = \Activitypub\Transformer\Factory::get_transformer( $wp_object ); + + // Check that we got the right transformer. + $this->assertInstanceOf( \Activitypub_Event_Extensions\Activitypub\Transformer\The_Events_Calendar::class, $transformer ); + } + + /** + * Test transformation of minimal event without venue. + */ + public function test_transform_of_minimal_event_without_venue() { + // Create a The Events Calendar Event. + $wp_object = tribe_events() + ->set_args( self::MOCKUP_EVENTS['minimal_event'] ) + ->create(); + + // Call the transformer. + $event_array = \Activitypub\Transformer\Factory::get_transformer( $wp_object )->to_object()->to_array(); + + // Check that the event ActivityStreams representation contains everything as expected. + $this->assertEquals( 'Event', $event_array['type'] ); + $this->assertEquals( 'My Event', $event_array['name'] ); + $this->assertEquals( '', $event_array['content'] ); + $this->assertEquals( gmdate( 'Y-m-d', strtotime( '+10 days 15:00:00' ) ) . 'T15:00:00Z', $event_array['startTime'] ); + $this->assertEquals( gmdate( 'Y-m-d', strtotime( '+10 days 16:00:00' ) ) . 'T16:00:00Z', $event_array['endTime'] ); + $this->assertTrue( $event_array['commentsEnabled'] ); + $this->assertEquals( 'allow_all', $event_array['repliesModerationOption'] ); + $this->assertEquals( 'free', $event_array['joinMode'] ); + $this->assertArrayNotHasKey( 'location', $event_array ); + $this->assertEquals( 'MEETING', $event_array['category'] ); + } + + /** + * Test transformation of event with mapped category. + */ + public function test_transform_event_with_mapped_categories() { + // Create category. + $category_id_music = wp_insert_term( 'Music', Tribe__Events__Main::TAXONOMY, array( 'slug' => 'music' ) ); + $category_id_theatre = wp_insert_term( 'Theatre', Tribe__Events__Main::TAXONOMY, array( 'slug' => 'theatre' ) ); + + // Set default mapping for event categories. + update_option( 'activitypub_event_extensions_default_event_category', 'MUSIC' ); + + // Set an override for the category with the slug theatre. + update_option( 'activitypub_event_extensions_event_category_mappings', array( 'theatre' => 'THEATRE' ) ); + + // Create a The Events Calendar event with the music category. + $wp_object = tribe_events() + ->set_args( self::MOCKUP_EVENTS['minimal_event'] ) + ->create(); + // Set the post term music to the event. + wp_set_post_terms( $wp_object->ID, $category_id_music['term_id'], Tribe__Events__Main::TAXONOMY ); + // Call the transformer. + $event_array = \Activitypub\Transformer\Factory::get_transformer( $wp_object )->to_object()->to_array(); + // See if the default category mapping is applied. + $this->assertEquals( 'MUSIC', $event_array['category'] ); + + // Set the post term theatre to the event. + wp_set_post_terms( $wp_object->ID, $category_id_theatre['term_id'], Tribe__Events__Main::TAXONOMY ); + // Call the transformer. + $event_array = \Activitypub\Transformer\Factory::get_transformer( $wp_object )->to_object()->to_array(); + // See if the default category mapping is applied. + $this->assertEquals( 'THEATRE', $event_array['category'] ); + } + + /** + * Test transformation of minimal event with minimal venue. + */ + public function test_transform_of_minimal_event_with_venue() { + // Create Venue. + $venue = tribe_venues()->set_args( self::MOCKUP_VENUS['minimal_venue'] )->create(); + // Create a The Events Calendar Event. + $wp_object = tribe_events() + ->set_args( self::MOCKUP_EVENTS['complex_event'] ) + ->set( 'venue', $venue->ID ) + ->create(); + + // Call the transformer. + $event_array = \Activitypub\Transformer\Factory::get_transformer( $wp_object )->to_object()->to_array(); + + // Check that the event ActivityStreams representation contains everything as expected. + $this->assertEquals( 'Event', $event_array['type'] ); + $this->assertEquals( 'My Event', $event_array['name'] ); + $this->assertEquals( '', $event_array['content'] ); + $this->assertEquals( gmdate( 'Y-m-d', strtotime( '+10 days 15:00:00' ) ) . 'T15:00:00Z', $event_array['startTime'] ); + $this->assertEquals( gmdate( 'Y-m-d', strtotime( '+10 days 16:00:00' ) ) . 'T16:00:00Z', $event_array['endTime'] ); + $this->assertEquals( gmdate( 'Y-m-d', strtotime( '+10 days 16:00:00' ) ) . 'T16:00:00Z', $event_array['commentsEnabled'] ); + $this->assertEquals( gmdate( 'Y-m-d', strtotime( '+10 days 16:00:00' ) ) . 'T16:00:00Z', $event_array['endTime'] ); + $this->assertArrayHasKey( 'location', $event_array ); + $this->assertEquals( 'Place', $event_array['location']['type'] ); + $this->assertEquals( self::MOCKUP_VENUS['minimal_venue']['venue'], $event_array['location']['name'] ); + } +} diff --git a/tests/test-class-sample.php b/tests/test-class-sample.php deleted file mode 100644 index 3e9bf11..0000000 --- a/tests/test-class-sample.php +++ /dev/null @@ -1,20 +0,0 @@ -assertTrue( true ); - } -}