diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 17902d48..c08f020b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -46,4 +46,6 @@ jobs: run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - name: Run tests + env: + WP_TESTS_DIR: /tmp/wordpress-tests-lib/ run: vendor/bin/phpunit diff --git a/.wp-env.json b/.wp-env.json index 2bf44411..eede0128 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -1,7 +1,8 @@ { - "core": "WordPress/WordPress#master", + "core": null, + "phpVersion": "8.3", "plugins": [ - "GlotPress/GlotPress#local", + "GlotPress/GlotPress", "https://downloads.wordpress.org/plugin/sqlite-object-cache.1.3.7.zip", "." ], diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh index c6f53dc5..c6417b0d 100755 --- a/bin/install-wp-tests.sh +++ b/bin/install-wp-tests.sh @@ -176,6 +176,11 @@ install_db() { fi } +install_gp() { + git clone --branch develop --single-branch -q https://github.com/GlotPress/GlotPress.git "$WP_CORE_DIR/wp-content/plugins/glotpress" +} + install_wp install_test_suite install_db +install_gp diff --git a/bin/run-tests.sh b/bin/run-tests.sh deleted file mode 100755 index 9aeab8c5..00000000 --- a/bin/run-tests.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -# This is only for use in development environments, it's not used in CI. -# You can call this with: composer dev:test - -set -ex - -wp-env run tests-cli --env-cwd=wp-content/plugins/wporg-gp-translation-events sh -c 'wp db query < schema.sql' -wp-env run tests-cli --env-cwd=wp-content/plugins/wporg-gp-translation-events ./vendor/bin/phpunit diff --git a/composer.json b/composer.json index b6a3b1f9..504366c3 100644 --- a/composer.json +++ b/composer.json @@ -10,11 +10,11 @@ "scripts":{ "lint": "phpcs --standard=phpcs.xml -s", "lint:fix": "phpcbf --standard=phpcs.xml", - "dev:start": "wp-env start --debug && wp-env run cli wp rewrite structure '/%postname%/'", + "dev:start": "wp-env start && wp-env run cli wp rewrite structure '/%postname%/'", "dev:debug": "wp-env start --xdebug", "dev:stop": "wp-env stop", "dev:db:schema": "wp-env run cli --env-cwd=wp-content/plugins/wporg-gp-translation-events sh -c 'wp db query < schema.sql'", - "dev:test": "bin/run-tests.sh" + "dev:test": "wp-env run tests-cli --env-cwd=wp-content/plugins/wporg-gp-translation-events ./vendor/bin/phpunit" }, "config": { "allow-plugins": { diff --git a/includes/translation-listener.php b/includes/stats-listener.php similarity index 99% rename from includes/translation-listener.php rename to includes/stats-listener.php index 011e4389..2eb1af9d 100644 --- a/includes/translation-listener.php +++ b/includes/stats-listener.php @@ -8,7 +8,7 @@ use GP_Translation; use GP_Translation_Set; -class Translation_Listener { +class Stats_Listener { const ACTION_CREATE = 'create'; const ACTION_APPROVE = 'approve'; const ACTION_REJECT = 'reject'; diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ac7184ac..e324c36e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,8 +8,8 @@ convertWarningsToExceptions="true" > - - ./tests/unit/ + + ./tests/ diff --git a/tests/bootstrap.php b/tests/bootstrap.php index b0a0c0a0..5f6a4cc2 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -5,6 +5,15 @@ $_tests_dir = rtrim( sys_get_temp_dir(), '/\\' ) . '/wordpress-tests-lib'; } +function _glotpress_path( string $path ): string { + $glotpress_path = dirname( __DIR__, 2 ) . '/glotpress/'; + if ( getenv( 'GITHUB_ACTIONS' ) ) { + $glotpress_path = '/tmp/wordpress/wp-content/plugins/glotpress/'; + } + + return $glotpress_path . $path; +} + // Forward custom PHPUnit Polyfills configuration to PHPUnit bootstrap file. $_phpunit_polyfills_path = getenv( 'WP_TESTS_PHPUNIT_POLYFILLS_PATH' ); if ( false !== $_phpunit_polyfills_path ) { @@ -19,14 +28,42 @@ // Give access to tests_add_filter() function. require_once "$_tests_dir/includes/functions.php"; +function _apply_plugin_schema() { + $schema_path = __DIR__ . '/../schema.sql'; + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents + $statements = explode( ';', file_get_contents( $schema_path ) ); + + global $wpdb; + foreach ( $statements as $statement ) { + $sql = trim( $statement ); + if ( ! $sql ) { + continue; + } + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( $sql ); + // phpcs:enable + } +} +tests_add_filter( 'muplugins_loaded', '_apply_plugin_schema' ); + /** * Manually load the plugin being tested. */ function _manually_load_plugin() { - require dirname( __DIR__ ) . '/wporg-gp-translation-events.php'; + require_once _glotpress_path( '/tests/phpunit/includes/loader.php' ); + require_once dirname( __DIR__ ) . '/wporg-gp-translation-events.php'; } - tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' ); +global $wp_tests_options; +$wp_tests_options['permalink_structure'] = '/%postname%'; + // Start up the WP testing environment. require "$_tests_dir/includes/bootstrap.php"; + +// Require GlotPress test code. +require_once _glotpress_path( '/tests/phpunit/lib/testcase.php' ); +require_once _glotpress_path( '/tests/phpunit/lib/testcase-route.php' ); +require_once _glotpress_path( '/tests/phpunit/lib/testcase-request.php' ); diff --git a/tests/lib/event-factory.php b/tests/lib/event-factory.php new file mode 100644 index 00000000..e1e630ca --- /dev/null +++ b/tests/lib/event-factory.php @@ -0,0 +1,94 @@ +default_generation_definitions = array( + 'post_status' => 'publish', + 'post_title' => new WP_UnitTest_Generator_Sequence( 'Event title %s' ), + 'post_content' => new WP_UnitTest_Generator_Sequence( 'Event content %s' ), + 'post_excerpt' => new WP_UnitTest_Generator_Sequence( 'Event excerpt %s' ), + 'post_type' => 'event', + ); + } + + public function create_draft(): int { + $now = new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) ); + + $event_id = $this->create_event( + $now->modify( '-1 hours' ), + $now->modify( '+1 hours' ), + array(), + ); + + $event = get_post( $event_id ); + $event->post_status = 'draft'; + wp_update_post( $event ); + + return $event_id; + } + + public function create_active( array $attendee_ids = array() ): int { + $now = new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) ); + + return $this->create_event( + $now->modify( '-1 hours' ), + $now->modify( '+1 hours' ), + $attendee_ids, + ); + } + + public function create_inactive_past( array $attendee_ids = array() ): int { + $now = new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) ); + + return $this->create_event( + $now->modify( '-2 hours' ), + $now->modify( '-1 hours' ), + $attendee_ids, + ); + } + + public function create_inactive_future( array $attendee_ids = array() ): int { + $now = new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) ); + + return $this->create_event( + $now->modify( '+1 hours' ), + $now->modify( '+2 hours' ), + $attendee_ids, + ); + } + + private function create_event( DateTimeImmutable $start, DateTimeImmutable $end, array $attendee_ids ): int { + $event_id = $this->create(); + $meta_key = Route::USER_META_KEY_ATTENDING; + + $user_id = get_current_user_id(); + if ( ! in_array( $user_id, $attendee_ids, true ) ) { + // The current user will have been added as attending the event, but it was not specified as an attendee by + // the caller of this function. So we remove the current user as attendee. + $event_ids = get_user_meta( $user_id, $meta_key, true ); + unset( $event_ids[ $event_id ] ); + update_user_meta( $user_id, $meta_key, array() ); + } + + update_post_meta( $event_id, '_event_start', $start->format( 'Y-m-d H:i:s' ) ); + update_post_meta( $event_id, '_event_end', $end->format( 'Y-m-d H:i:s' ) ); + update_post_meta( $event_id, '_event_timezone', 'Europe/Lisbon' ); + + foreach ( $attendee_ids as $user_id ) { + $event_ids = get_user_meta( $user_id, $meta_key, true ) ?: array(); + $event_ids[] = $event_id; + update_user_meta( $user_id, $meta_key, $event_ids ); + } + + return $event_id; + } +} diff --git a/tests/lib/translation-factory.php b/tests/lib/translation-factory.php new file mode 100644 index 00000000..c1ad71fa --- /dev/null +++ b/tests/lib/translation-factory.php @@ -0,0 +1,34 @@ +gp_factory = $gp_factory; + $this->set = $this->gp_factory->translation_set->create_with_project_and_locale(); + } + + public function create( int $user_id ) { + $original = $this->gp_factory->original->create( + array( + 'project_id' => $this->set->project->id, + 'status' => '+active', + 'singular' => 'foo', + ) + ); + + return $this->gp_factory->translation->create( + array( + 'user_id' => $user_id, + 'translation_set_id' => $this->set->id, + 'original_id' => $original->id, + 'status' => 'waiting', + ) + ); + } +} diff --git a/tests/stats-listener.php b/tests/stats-listener.php new file mode 100644 index 00000000..d0b6a8cc --- /dev/null +++ b/tests/stats-listener.php @@ -0,0 +1,207 @@ +translation_factory = new Translation_Factory( $this->factory ); + $this->event_factory = new Event_Factory(); + } + + private function get_stats(): array { + global $wpdb; + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching + return $wpdb->get_results( 'select * from wp_wporg_gp_translation_events_actions', ARRAY_A ); + // phpcs:enable + } + + private function clean_stats() { + global $wpdb; + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->query( 'delete from wp_wporg_gp_translation_events_actions' ); + // phpcs:enable + } + + public function test_does_not_store_action_for_draft_events() { + $this->set_normal_user_as_current(); + $user_id = wp_get_current_user()->ID; + + $this->event_factory->create_draft(); + $this->event_factory->create_draft(); + + $this->translation_factory->create( $user_id ); + // Stats_Listener will have been called. + + $stats = $this->get_stats(); + $this->assertEmpty( $stats ); + } + + public function test_does_not_store_action_for_inactive_events() { + $this->set_normal_user_as_current(); + $user_id = wp_get_current_user()->ID; + + $this->event_factory->create_inactive_past( array( $user_id ) ); + $this->event_factory->create_inactive_future( array( $user_id ) ); + + $this->translation_factory->create( $user_id ); + // Stats_Listener will have been called. + + $stats = $this->get_stats(); + $this->assertEmpty( $stats ); + } + + public function test_does_not_store_action_if_user_not_attending() { + $this->set_normal_user_as_current(); + $user_id = wp_get_current_user()->ID; + + $this->event_factory->create_active(); + $this->event_factory->create_active(); + + $this->translation_factory->create( $user_id ); + // Stats_Listener will have been called. + + $stats = $this->get_stats(); + $this->assertEmpty( $stats ); + } + + public function test_stores_action_create() { + $this->set_normal_user_as_current(); + $user_id = wp_get_current_user()->ID; + + $event1_id = $this->event_factory->create_active( array( $user_id ) ); + $event2_id = $this->event_factory->create_active( array( $user_id ) ); + + $translation = $this->translation_factory->create( $user_id ); + // Stats_Listener will have been called. + + $stats = $this->get_stats(); + $this->assertCount( 2, $stats ); + + $event1_stats = $stats[0]; + $this->assertEquals( $event1_id, $event1_stats['event_id'] ); + $this->assertEquals( $user_id, $event1_stats['user_id'] ); + $this->assertEquals( $translation->id, $event1_stats['translation_id'] ); + $this->assertEquals( 'create', $event1_stats['action'] ); + $this->assertEquals( 'aa', $event1_stats['locale'] ); + + $event2_stats = $stats[1]; + $this->assertEquals( $event2_id, $event2_stats['event_id'] ); + $this->assertEquals( $user_id, $event2_stats['user_id'] ); + $this->assertEquals( $translation->id, $event2_stats['translation_id'] ); + $this->assertEquals( 'create', $event2_stats['action'] ); + $this->assertEquals( 'aa', $event2_stats['locale'] ); + } + + public function test_stores_action_approve() { + $this->set_normal_user_as_current(); + $user_id = wp_get_current_user()->ID; + + $event1_id = $this->event_factory->create_active( array( $user_id ) ); + $event2_id = $this->event_factory->create_active( array( $user_id ) ); + + /** @var GP_Translation $translation */ + $translation = $this->translation_factory->create( $user_id ); + // Stats_Listener will have been called. + // Clean up stats because we won't care about the "created" action. + $this->clean_stats(); + + $translation->set_as_current(); + // Stats_Listener will have been called. + + $stats = $this->get_stats(); + $this->assertCount( 2, $stats ); + + $event1_stats = $stats[0]; + $this->assertEquals( $event1_id, $event1_stats['event_id'] ); + $this->assertEquals( $user_id, $event1_stats['user_id'] ); + $this->assertEquals( $translation->id, $event1_stats['translation_id'] ); + $this->assertEquals( 'approve', $event1_stats['action'] ); + $this->assertEquals( 'aa', $event1_stats['locale'] ); + + $event2_stats = $stats[1]; + $this->assertEquals( $event2_id, $event2_stats['event_id'] ); + $this->assertEquals( $user_id, $event2_stats['user_id'] ); + $this->assertEquals( $translation->id, $event2_stats['translation_id'] ); + $this->assertEquals( 'approve', $event2_stats['action'] ); + $this->assertEquals( 'aa', $event2_stats['locale'] ); + } + + public function test_stores_action_reject() { + $this->set_normal_user_as_current(); + $user_id = wp_get_current_user()->ID; + + $event1_id = $this->event_factory->create_active( array( $user_id ) ); + $event2_id = $this->event_factory->create_active( array( $user_id ) ); + + /** @var GP_Translation $translation */ + $translation = $this->translation_factory->create( $user_id ); + // Stats_Listener will have been called. + // Clean up stats because we won't care about the "created" action. + $this->clean_stats(); + + $translation->reject(); + // Stats_Listener will have been called. + + $stats = $this->get_stats(); + $this->assertCount( 2, $stats ); + + $event1_stats = $stats[0]; + $this->assertEquals( $event1_id, $event1_stats['event_id'] ); + $this->assertEquals( $user_id, $event1_stats['user_id'] ); + $this->assertEquals( $translation->id, $event1_stats['translation_id'] ); + $this->assertEquals( 'reject', $event1_stats['action'] ); + $this->assertEquals( 'aa', $event1_stats['locale'] ); + + $event2_stats = $stats[1]; + $this->assertEquals( $event2_id, $event2_stats['event_id'] ); + $this->assertEquals( $user_id, $event2_stats['user_id'] ); + $this->assertEquals( $translation->id, $event2_stats['translation_id'] ); + $this->assertEquals( 'reject', $event2_stats['action'] ); + $this->assertEquals( 'aa', $event2_stats['locale'] ); + } + + public function test_stores_action_request_changes() { + $this->set_normal_user_as_current(); + $user_id = wp_get_current_user()->ID; + + $event1_id = $this->event_factory->create_active( array( $user_id ) ); + $event2_id = $this->event_factory->create_active( array( $user_id ) ); + + /** @var GP_Translation $translation */ + $translation = $this->translation_factory->create( $user_id ); + // Stats_Listener will have been called. + // Clean up stats because we won't care about the "created" action. + $this->clean_stats(); + + $translation->set_as_changesrequested(); + // Stats_Listener will have been called. + + $stats = $this->get_stats(); + $this->assertCount( 2, $stats ); + + $event1_stats = $stats[0]; + $this->assertEquals( $event1_id, $event1_stats['event_id'] ); + $this->assertEquals( $user_id, $event1_stats['user_id'] ); + $this->assertEquals( $translation->id, $event1_stats['translation_id'] ); + $this->assertEquals( 'request_changes', $event1_stats['action'] ); + $this->assertEquals( 'aa', $event1_stats['locale'] ); + + $event2_stats = $stats[1]; + $this->assertEquals( $event2_id, $event2_stats['event_id'] ); + $this->assertEquals( $user_id, $event2_stats['user_id'] ); + $this->assertEquals( $translation->id, $event2_stats['translation_id'] ); + $this->assertEquals( 'request_changes', $event2_stats['action'] ); + $this->assertEquals( 'aa', $event2_stats['locale'] ); + } +} diff --git a/tests/unit/dummy.php b/tests/unit/dummy.php deleted file mode 100644 index 671fbf68..00000000 --- a/tests/unit/dummy.php +++ /dev/null @@ -1,14 +0,0 @@ -assertTrue( true ); - } -} diff --git a/wporg-gp-translation-events.php b/wporg-gp-translation-events.php index d566fc47..698c72fa 100644 --- a/wporg-gp-translation-events.php +++ b/wporg-gp-translation-events.php @@ -392,7 +392,7 @@ function () { require_once __DIR__ . '/includes/event.php'; require_once __DIR__ . '/includes/route.php'; require_once __DIR__ . '/includes/stats-calculator.php'; - require_once __DIR__ . '/includes/translation-listener.php'; + require_once __DIR__ . '/includes/stats-listener.php'; GP::$router->add( '/events?', array( 'Wporg\TranslationEvents\Route', 'events_list' ) ); GP::$router->add( '/events/new', array( 'Wporg\TranslationEvents\Route', 'events_create' ) ); @@ -401,9 +401,9 @@ function () { GP::$router->add( '/events/my-events', array( 'Wporg\TranslationEvents\Route', 'events_user_created' ) ); GP::$router->add( '/events/([a-z0-9_-]+)', array( 'Wporg\TranslationEvents\Route', 'events_details' ) ); - $active_events_cache = new Active_Events_Cache(); - $wporg_gp_translation_events_listener = new Translation_Listener( $active_events_cache ); - $wporg_gp_translation_events_listener->start(); + $active_events_cache = new Active_Events_Cache(); + $stats_listener = new Stats_Listener( $active_events_cache ); + $stats_listener->start(); } );