diff --git a/includes/class-sensei-cli.php b/includes/class-sensei-cli.php
index 4b5667d8d7..20f0c0a964 100644
--- a/includes/class-sensei-cli.php
+++ b/includes/class-sensei-cli.php
@@ -5,7 +5,9 @@
* @package sensei
*/
-defined( 'ABSPATH' ) || exit;
+if ( ! defined( 'ABSPATH' ) ) {
+ exit; // Exit if accessed directly.
+}
/**
* CLI class.
@@ -17,21 +19,14 @@ class Sensei_CLI {
* Class constructor.
*/
public function __construct() {
- $this->load();
$this->register();
}
- /**
- * Load the command files.
- */
- private function load() {
- require_once dirname( __FILE__ ) . '/cli/class-sensei-db-seed-command.php';
- }
-
/**
* Register the CLI commands.
*/
private function register() {
WP_CLI::add_command( 'sensei db seed', Sensei_DB_Seed_Command::class );
+ WP_CLI::add_command( 'sensei validate progress', Sensei_Validate_Progress_Command::class );
}
}
diff --git a/includes/class-sensei-course.php b/includes/class-sensei-course.php
index 5b8d59734c..3f5e168cd9 100755
--- a/includes/class-sensei-course.php
+++ b/includes/class-sensei-course.php
@@ -2302,9 +2302,7 @@ public function load_user_courses_content( $user = false ) {
* Returns a list of all courses
*
* @since 1.8.0
- * @return array $courses{
- * @type $course WP_Post
- * }
+ * @return WP_Post[]
*/
public static function get_all_courses() {
diff --git a/includes/cli/class-sensei-db-seed-command.php b/includes/cli/class-sensei-db-seed-command.php
index 1a01b1c14d..a2df020387 100644
--- a/includes/cli/class-sensei-db-seed-command.php
+++ b/includes/cli/class-sensei-db-seed-command.php
@@ -5,7 +5,9 @@
* @package sensei
*/
-defined( 'ABSPATH' ) || exit;
+if ( ! defined( 'ABSPATH' ) ) {
+ exit; // Exit if accessed directly.
+}
/**
* WP-CLI command that helps with seeding the database.
diff --git a/includes/cli/class-sensei-validate-progress-command.php b/includes/cli/class-sensei-validate-progress-command.php
new file mode 100644
index 0000000000..26c5c94207
--- /dev/null
+++ b/includes/cli/class-sensei-validate-progress-command.php
@@ -0,0 +1,66 @@
+run();
+
+ if ( ! $progress_validation->has_errors() ) {
+ WP_CLI::success( 'Progress data is valid.' );
+ return;
+ }
+
+ $this->output_validation_errors( $progress_validation );
+
+ WP_CLI::error( 'Progress data is not valid.' );
+ }
+
+ /**
+ * Output the validation errors.
+ *
+ * @since $$next-version$$
+ *
+ * @param Progress_Validation $progress_validation Progress validation.
+ */
+ private function output_validation_errors( Progress_Validation $progress_validation ) {
+ foreach ( $progress_validation->get_errors() as $error ) {
+ WP_CLI::warning( $error->get_message() );
+
+ if ( $error->has_data() ) {
+ $error_data = $error->get_data();
+ $error_data = is_array( $error_data[0] ) ? $error_data : [ $error_data ];
+
+ WP_CLI\Utils\format_items(
+ 'table',
+ $error_data,
+ array_keys( $error_data[0] )
+ );
+ }
+ }
+ }
+}
diff --git a/includes/internal/migration/migrations/class-student-progress-migration.php b/includes/internal/migration/migrations/class-student-progress-migration.php
index b038654cde..7af3ee4b8d 100644
--- a/includes/internal/migration/migrations/class-student-progress-migration.php
+++ b/includes/internal/migration/migrations/class-student-progress-migration.php
@@ -122,7 +122,7 @@ private function get_comments_and_meta( int $after_comment_id, bool $dry_run ):
// At the moment we don't care about post meta for course progress.
if ( 'sensei_lesson_status' === $progress_comment->comment_type ) {
// Map the post ID to the comment ID. Is used later to map post meta to the comment ID.
- $post_ids[ $progress_comment->comment_post_ID ] = $progress_comment->comment_ID;
+ $post_ids[ $progress_comment->comment_post_ID ][] = $progress_comment->comment_ID;
}
}
@@ -188,8 +188,10 @@ private function get_comments_and_meta( int $after_comment_id, bool $dry_run ):
$mapped_meta[ $comment_id ]['status'] = $comment_status;
}
} else {
- // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
- $mapped_meta[ $comment_id ][ $meta->meta_key ] = $meta->meta_value;
+ foreach ( $post_ids[ $meta->post_id ] as $comment_id ) {
+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
+ $mapped_meta[ $comment_id ][ $meta->meta_key ] = $meta->meta_value;
+ }
}
}
diff --git a/includes/internal/migration/validations/class-progress-validation.php b/includes/internal/migration/validations/class-progress-validation.php
new file mode 100644
index 0000000000..1510ac7695
--- /dev/null
+++ b/includes/internal/migration/validations/class-progress-validation.php
@@ -0,0 +1,392 @@
+errors = array();
+
+ if ( ! $this->is_progress_migration_complete() ) {
+ $this->add_error( 'The progress migration is not complete. Please run the progress migration first.' );
+ }
+
+ foreach ( $this->get_course_ids() as $course_id ) {
+ $this->validate_course_progress( $course_id );
+ }
+
+ foreach ( $this->get_lesson_ids() as $lesson_id ) {
+ $this->validate_lesson_progress( $lesson_id );
+ }
+
+ foreach ( $this->get_quiz_ids() as $quiz_id ) {
+ $this->validate_quiz_progress( $quiz_id );
+ }
+ }
+
+ /**
+ * Check if there are validation errors.
+ *
+ * @internal
+ *
+ * @since $$next-version$$
+ *
+ * @return bool
+ */
+ public function has_errors(): bool {
+ return (bool) $this->errors;
+ }
+
+ /**
+ * Get the validation errors.
+ *
+ * @internal
+ *
+ * @since $$next-version$$
+ *
+ * @return Validation_Error[]
+ */
+ public function get_errors(): array {
+ return $this->errors;
+ }
+
+ /**
+ * Add a validation error.
+ *
+ * @param string $message Error message.
+ * @param array $data Error data.
+ */
+ private function add_error( string $message, array $data = [] ): void {
+ $this->errors[] = new Validation_Error( $message, $data );
+ }
+
+ /**
+ * Get the course IDs.
+ *
+ * @psalm-suppress InvalidReturnType, InvalidReturnStatement -- Psalm doesn't understand the 'fields' argument.
+ *
+ * @return int[]
+ */
+ private function get_course_ids(): array {
+ return get_posts(
+ [
+ 'post_type' => 'course',
+ 'post_status' => 'any',
+ 'posts_per_page' => -1,
+ 'fields' => 'ids',
+ ]
+ );
+ }
+
+ /**
+ * Get the lesson IDs.
+ *
+ * @psalm-suppress InvalidReturnType, InvalidReturnStatement -- Psalm doesn't understand the 'fields' argument.
+ *
+ * @return int[]
+ */
+ private function get_lesson_ids(): array {
+ return get_posts(
+ [
+ 'post_type' => 'lesson',
+ 'post_status' => 'any',
+ 'posts_per_page' => -1,
+ 'fields' => 'ids',
+ ]
+ );
+ }
+
+ /**
+ * Get the quiz IDs.
+ *
+ * @psalm-suppress InvalidReturnType, InvalidReturnStatement -- Psalm doesn't understand the 'fields' argument.
+ *
+ * @return int[]
+ */
+ private function get_quiz_ids(): array {
+ return get_posts(
+ [
+ 'post_type' => 'quiz',
+ 'post_status' => 'any',
+ 'posts_per_page' => -1,
+ 'fields' => 'ids',
+ ]
+ );
+ }
+
+ /**
+ * Check if the progress migration is complete.
+ *
+ * @return bool
+ */
+ private function is_progress_migration_complete(): bool {
+ return (bool) get_option( Migration_Job_Scheduler::COMPLETED_OPTION_NAME, false );
+ }
+
+ /**
+ * Validate the course progress.
+ *
+ * @param int $course_id Course post ID.
+ */
+ private function validate_course_progress( int $course_id ): void {
+ global $wpdb;
+ // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
+ $user_ids = $wpdb->get_col(
+ $wpdb->prepare(
+ "SELECT user_id FROM {$wpdb->comments}
+ WHERE comment_type = 'sensei_course_status'
+ AND comment_post_ID = %d",
+ $course_id
+ )
+ );
+ if ( ! $user_ids ) {
+ return;
+ }
+
+ $comments_based_repository = new Comments_Based_Course_Progress_Repository();
+ $tables_based_repository = new Tables_Based_Course_Progress_Repository( $wpdb );
+
+ foreach ( $user_ids as $user_id ) {
+ $comments_based_progress = $comments_based_repository->get( $course_id, $user_id );
+ $tables_based_progress = $tables_based_repository->get( $course_id, $user_id );
+
+ if ( ! $comments_based_progress ) {
+ $this->add_error(
+ 'Course comments based progress not found.',
+ [
+ 'course_id' => $course_id,
+ 'user_id' => $user_id,
+ ]
+ );
+ continue;
+ }
+
+ if ( ! $tables_based_progress ) {
+ $this->add_error(
+ 'Course tables based progress not found.',
+ [
+ 'course_id' => $course_id,
+ 'user_id' => $user_id,
+ ]
+ );
+ continue;
+ }
+
+ $this->compare_progress( $comments_based_progress, $tables_based_progress );
+ }
+ }
+
+ /**
+ * Validate the lesson progress.
+ *
+ * @param int $lesson_id Lesson post ID.
+ */
+ private function validate_lesson_progress( int $lesson_id ): void {
+ global $wpdb;
+ // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
+ $user_ids = $wpdb->get_col(
+ $wpdb->prepare(
+ "SELECT user_id FROM {$wpdb->comments}
+ WHERE comment_type = 'sensei_lesson_status'
+ AND comment_post_ID = %d",
+ $lesson_id
+ )
+ );
+ if ( ! $user_ids ) {
+ return;
+ }
+
+ $comments_based_repository = new Comments_Based_Lesson_Progress_Repository();
+ $tables_based_repository = new Tables_Based_Lesson_Progress_Repository( $wpdb );
+
+ foreach ( $user_ids as $user_id ) {
+ $comments_based_progress = $comments_based_repository->get( $lesson_id, $user_id );
+ $tables_based_progress = $tables_based_repository->get( $lesson_id, $user_id );
+
+ if ( ! $comments_based_progress ) {
+ $this->add_error(
+ 'Lesson comments based progress not found.',
+ [
+ 'lesson_id' => $lesson_id,
+ 'user_id' => $user_id,
+ ]
+ );
+ continue;
+ }
+
+ if ( ! $tables_based_progress ) {
+ $this->add_error(
+ 'Lesson tables based progress not found.',
+ [
+ 'lesson_id' => $lesson_id,
+ 'user_id' => $user_id,
+ ]
+ );
+ continue;
+ }
+
+ $this->compare_progress( $comments_based_progress, $tables_based_progress );
+ }
+ }
+
+ /**
+ * Validate the quiz progress.
+ *
+ * @param int $quiz_id Quiz post ID.
+ */
+ private function validate_quiz_progress( int $quiz_id ): void {
+ $lesson_id = Sensei()->quiz->get_lesson_id( $quiz_id );
+
+ global $wpdb;
+ // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
+ $user_ids = $wpdb->get_col(
+ $wpdb->prepare(
+ "SELECT user_id FROM {$wpdb->comments}
+ WHERE comment_type = 'sensei_lesson_status'
+ AND comment_post_ID = %d",
+ $lesson_id
+ )
+ );
+ if ( ! $user_ids ) {
+ return;
+ }
+
+ $comments_based_repository = new Comments_Based_Quiz_Progress_Repository();
+ $tables_based_repository = new Tables_Based_Quiz_Progress_Repository( $wpdb );
+
+ foreach ( $user_ids as $user_id ) {
+ $comments_based_progress = $comments_based_repository->get( $quiz_id, $user_id );
+ $tables_based_progress = $tables_based_repository->get( $quiz_id, $user_id );
+
+ if ( ! $comments_based_progress ) {
+ $this->add_error(
+ 'Quiz comments based progress not found.',
+ [
+ 'quiz_id' => $quiz_id,
+ 'user_id' => $user_id,
+ ]
+ );
+ continue;
+ }
+
+ if ( ! $tables_based_progress ) {
+ $this->add_error(
+ 'Quiz tables based progress not found.',
+ [
+ 'quiz_id' => $quiz_id,
+ 'user_id' => $user_id,
+ ]
+ );
+ continue;
+ }
+
+ $this->compare_progress( $comments_based_progress, $tables_based_progress );
+ }
+ }
+
+ /**
+ * Compare the comments and tables based progress.
+ *
+ * @param object $comments_based_progress Comments based progress.
+ * @param object $tables_based_progress Tables based progress.
+ */
+ private function compare_progress( object $comments_based_progress, object $tables_based_progress ): void {
+ // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison -- Intended.
+ if ( $this->get_progress_data( $comments_based_progress ) != $this->get_progress_data( $tables_based_progress ) ) {
+ $this->add_mismatch_error( $comments_based_progress, $tables_based_progress );
+ }
+ }
+
+ /**
+ * Get the progress data.
+ *
+ * @param object $progress Progress.
+ *
+ * @return array
+ * @throws \InvalidArgumentException When invalid progress type is provided.
+ */
+ private function get_progress_data( object $progress ): array {
+ if ( $progress instanceof Course_Progress_Interface ) {
+ $type = 'course';
+ $post_id = $progress->get_course_id();
+ } elseif ( $progress instanceof Lesson_Progress_Interface ) {
+ $type = 'lesson';
+ $post_id = $progress->get_lesson_id();
+ } elseif ( $progress instanceof Quiz_Progress_Interface ) {
+ $type = 'quiz';
+ $post_id = $progress->get_quiz_id();
+ } else {
+ throw new \InvalidArgumentException( 'Invalid progress type.' );
+ }
+
+ return [
+ 'type' => $type,
+ 'post_id' => $post_id,
+ 'user_id' => $progress->get_user_id(),
+ 'status' => $progress->get_status(),
+ 'started_at' => $progress->get_started_at(),
+ 'completed_at' => $progress->get_completed_at(),
+ ];
+ }
+
+ /**
+ * Log a progress mismatch.
+ *
+ * @param object $comments_based_progress Comments based progress.
+ * @param object $tables_based_progress Tables based progress.
+ */
+ private function add_mismatch_error( object $comments_based_progress, object $tables_based_progress ): void {
+ $this->add_error(
+ 'Data mismatch between comments and tables based progress.',
+ [
+ array_merge(
+ [
+ 'source' => 'comments',
+ ],
+ $this->get_progress_data( $comments_based_progress )
+ ),
+ array_merge(
+ [
+ 'source' => 'tables',
+ ],
+ $this->get_progress_data( $tables_based_progress )
+ ),
+ ]
+ );
+ }
+}
diff --git a/includes/internal/migration/validations/class-validation-error.php b/includes/internal/migration/validations/class-validation-error.php
new file mode 100644
index 0000000000..e8021d85cc
--- /dev/null
+++ b/includes/internal/migration/validations/class-validation-error.php
@@ -0,0 +1,86 @@
+message = $message;
+ $this->data = $data;
+ }
+
+ /**
+ * Get the error message.
+ *
+ * @internal
+ *
+ * @since $$next-version$$
+ *
+ * @return string
+ */
+ public function get_message(): string {
+ return $this->message;
+ }
+
+ /**
+ * Get the error data.
+ *
+ * @internal
+ *
+ * @since $$next-version$$
+ *
+ * @return array
+ */
+ public function get_data(): array {
+ return $this->data;
+ }
+
+ /**
+ * Check if there is error data.
+ *
+ * @internal
+ *
+ * @since $$next-version$$
+ *
+ * @return bool
+ */
+ public function has_data(): bool {
+ return (bool) $this->data;
+ }
+}
diff --git a/psalm.xml b/psalm.xml
index 43611cc0a8..9288bddc11 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -25,6 +25,7 @@
+
@@ -32,6 +33,20 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/unit-tests/internal/migration/migrations/test-class-student-progress-migration.php b/tests/unit-tests/internal/migration/migrations/test-class-student-progress-migration.php
index 821b153bd4..60136e86d8 100644
--- a/tests/unit-tests/internal/migration/migrations/test-class-student-progress-migration.php
+++ b/tests/unit-tests/internal/migration/migrations/test-class-student-progress-migration.php
@@ -4,6 +4,7 @@
use Sensei\Internal\Migration\Migrations\Student_Progress_Migration;
use Sensei_Factory;
+use Sensei_Utils;
/**
* Class Student_Progress_Migration_Test
@@ -61,8 +62,8 @@ public function testRun_CommentsExist_ReturnsMatchingNumberOfInserts(): void {
)
);
- \Sensei_Utils::start_user_on_course( 1, $course_id );
- \Sensei_Utils::user_start_lesson( 1, $lesson_id, true );
+ Sensei_Utils::start_user_on_course( 1, $course_id );
+ Sensei_Utils::user_start_lesson( 1, $lesson_id, true );
update_option( 'sensei_migrated_progress_last_comment_id', 0 );
@@ -94,11 +95,18 @@ public function testRun_CommentsExist_CreatesProgressMatchingEntriesInCustomTabl
),
)
);
+ $user_1 = $this->factory->user->create();
+ $user_2 = $this->factory->user->create();
+
update_post_meta( $quiz_id, '_quiz_lesson', $lesson_id );
- \Sensei_Utils::start_user_on_course( 1, $course_id );
- \Sensei_Utils::user_start_lesson( 1, $lesson_id, true );
- \Sensei_Utils::user_passed_quiz( $quiz_id, 1 );
+ Sensei_Utils::start_user_on_course( $user_1, $course_id );
+ Sensei_Utils::user_start_lesson( $user_1, $lesson_id, true );
+ Sensei_Utils::user_passed_quiz( $quiz_id, $user_1 );
+
+ Sensei_Utils::start_user_on_course( $user_2, $course_id );
+ Sensei_Utils::user_start_lesson( $user_2, $lesson_id, true );
+ Sensei_Utils::user_passed_quiz( $quiz_id, $user_2 );
update_option( 'sensei_migrated_progress_last_comment_id', 0 );
@@ -109,19 +117,37 @@ public function testRun_CommentsExist_CreatesProgressMatchingEntriesInCustomTabl
$actual_rows = $this->get_table_based_progress();
$expected = array(
array(
- 'user_id' => 1,
+ 'user_id' => $user_1,
+ 'post_id' => $course_id,
+ 'status' => 'in-progress',
+ 'type' => 'course',
+ ),
+ array(
+ 'user_id' => $user_1,
+ 'post_id' => $lesson_id,
+ 'status' => 'complete',
+ 'type' => 'lesson',
+ ),
+ array(
+ 'user_id' => $user_1,
+ 'post_id' => $quiz_id,
+ 'status' => 'passed',
+ 'type' => 'quiz',
+ ),
+ array(
+ 'user_id' => $user_2,
'post_id' => $course_id,
'status' => 'in-progress',
'type' => 'course',
),
array(
- 'user_id' => 1,
+ 'user_id' => $user_2,
'post_id' => $lesson_id,
'status' => 'complete',
'type' => 'lesson',
),
array(
- 'user_id' => 1,
+ 'user_id' => $user_2,
'post_id' => $quiz_id,
'status' => 'passed',
'type' => 'quiz',
@@ -133,7 +159,7 @@ public function testRun_CommentsExist_CreatesProgressMatchingEntriesInCustomTabl
private function get_table_based_progress(): array {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
- $rows = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}sensei_lms_progress" );
+ $rows = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}sensei_lms_progress ORDER BY user_id" );
$result = array();
foreach ( $rows as $row ) {
diff --git a/tests/unit-tests/internal/migration/migrations/validations/test-class-validation-error.php b/tests/unit-tests/internal/migration/migrations/validations/test-class-validation-error.php
new file mode 100644
index 0000000000..4c26ef5f6c
--- /dev/null
+++ b/tests/unit-tests/internal/migration/migrations/validations/test-class-validation-error.php
@@ -0,0 +1,67 @@
+get_message();
+
+ /* Assert. */
+ $this->assertSame( 'foo', $actual );
+ }
+
+ public function testGetData_HasData_ReturnsData(): void {
+ /* Arrange. */
+ $error = new Validation_Error( 'foo', [ 'bar' => 'baz' ] );
+
+ /* Act. */
+ $actual = $error->get_data();
+
+ /* Assert. */
+ $this->assertSame( [ 'bar' => 'baz' ], $actual );
+ }
+
+ public function testGetData_HasNoData_ReturnsEmptyArray(): void {
+ /* Arrange. */
+ $error = new Validation_Error( 'foo' );
+
+ /* Act. */
+ $actual = $error->get_data();
+
+ /* Assert. */
+ $this->assertSame( [], $actual );
+ }
+
+ public function testHasData_HasData_ReturnsTrue(): void {
+ /* Arrange. */
+ $error = new Validation_Error( 'foo', [ 'bar' => 'baz' ] );
+
+ /* Act. */
+ $actual = $error->has_data();
+
+ /* Assert. */
+ $this->assertTrue( $actual );
+ }
+
+ public function testHasData_HasNoData_ReturnsTrue(): void {
+ /* Arrange. */
+ $error = new Validation_Error( 'foo' );
+
+ /* Act. */
+ $actual = $error->has_data();
+
+ /* Assert. */
+ $this->assertFalse( $actual );
+ }
+}
diff --git a/tests/unit-tests/internal/migration/migrations/validations/test-progress-validation.php b/tests/unit-tests/internal/migration/migrations/validations/test-progress-validation.php
new file mode 100644
index 0000000000..bf9696b49a
--- /dev/null
+++ b/tests/unit-tests/internal/migration/migrations/validations/test-progress-validation.php
@@ -0,0 +1,267 @@
+factory = new Sensei_Factory();
+ }
+
+ public function testRun_WhenProgressMigrationIsComplete_HasNoErrors(): void {
+ /* Arrange. */
+ $progress_validation = new Progress_Validation();
+
+ $this->mask_migration_as_complete();
+
+ /* Act. */
+ $progress_validation->run();
+
+ /* Assert. */
+ $this->assertFalse( $progress_validation->has_errors() );
+ }
+
+ public function testRun_WhenProgressMigrationIsNotComplete_HasError(): void {
+ /* Arrange. */
+ $progress_validation = new Progress_Validation();
+
+ /* Act. */
+ $progress_validation->run();
+
+ /* Assert. */
+ $this->assertSame(
+ 'The progress migration is not complete. Please run the progress migration first.',
+ $this->get_first_error_message( $progress_validation )
+ );
+ }
+
+ public function testRun_WhenHasCourseProgressInTable_HasNoErrors(): void {
+ /* Arrange. */
+ $course_id = $this->factory->course->create();
+ $progress_validation = new Progress_Validation();
+
+ $this->directlyEnrolStudent( 1, $course_id );
+ $this->mask_migration_as_complete();
+
+ /* Act. */
+ $progress_validation->run();
+
+ /* Assert. */
+ $this->assertFalse( $progress_validation->has_errors() );
+ }
+
+ public function testRun_WhenHasNoCourseProgressInTable_HasError(): void {
+ /* Arrange. */
+ global $wpdb;
+ $course_id = $this->factory->course->create();
+ $progress_validation = new Progress_Validation();
+ $progress_repository = new Tables_Based_Course_Progress_Repository( $wpdb );
+
+ $this->directlyEnrolStudent( 1, $course_id );
+ $progress_repository->delete_for_course( $course_id );
+ $this->mask_migration_as_complete();
+
+ /* Act. */
+ $progress_validation->run();
+
+ /* Assert. */
+ $this->assertSame(
+ 'Course tables based progress not found.',
+ $this->get_first_error_message( $progress_validation )
+ );
+ }
+
+ public function testRun_WhenHasCourseProgressButDataIsNotMatching_HasError(): void {
+ /* Arrange. */
+ global $wpdb;
+ $course_id = $this->factory->course->create();
+ $progress_validation = new Progress_Validation();
+ $progress_repository = new Tables_Based_Course_Progress_Repository( $wpdb );
+
+ $this->directlyEnrolStudent( 1, $course_id );
+ $progress = $progress_repository->get( $course_id, 1 );
+ $progress->complete();
+ $progress_repository->save( $progress );
+ $this->mask_migration_as_complete();
+
+ /* Act. */
+ $progress_validation->run();
+
+ /* Assert. */
+ $this->assertSame(
+ 'Data mismatch between comments and tables based progress.',
+ $this->get_first_error_message( $progress_validation )
+ );
+ }
+
+ public function testRun_WhenHasLessonProgressInTable_HasNoErrors(): void {
+ /* Arrange. */
+ $lesson_id = $this->factory->lesson->create();
+ $progress_validation = new Progress_Validation();
+
+ Sensei_Utils::user_start_lesson( 1, $lesson_id );
+ $this->mask_migration_as_complete();
+
+ /* Act. */
+ $progress_validation->run();
+
+ /* Assert. */
+ $this->assertFalse( $progress_validation->has_errors() );
+ }
+
+ public function testRun_WhenHasNoLessonProgressInTable_HasError(): void {
+ /* Arrange. */
+ global $wpdb;
+ $lesson_id = $this->factory->lesson->create();
+ $progress_validation = new Progress_Validation();
+ $progress_repository = new Tables_Based_Lesson_Progress_Repository( $wpdb );
+
+ Sensei_Utils::user_start_lesson( 1, $lesson_id );
+ $progress_repository->delete_for_lesson( $lesson_id );
+ $this->mask_migration_as_complete();
+
+ /* Act. */
+ $progress_validation->run();
+
+ /* Assert. */
+ $this->assertSame(
+ 'Lesson tables based progress not found.',
+ $this->get_first_error_message( $progress_validation )
+ );
+ }
+
+ public function testRun_WhenHasLessonProgressButDataIsNotMatching_HasError(): void {
+ /* Arrange. */
+ global $wpdb;
+ $lesson_id = $this->factory->lesson->create();
+ $progress_validation = new Progress_Validation();
+ $progress_repository = new Tables_Based_Lesson_Progress_Repository( $wpdb );
+
+ Sensei_Utils::user_start_lesson( 1, $lesson_id );
+ $progress = $progress_repository->get( $lesson_id, 1 );
+ $progress->complete();
+ $progress_repository->save( $progress );
+ $this->mask_migration_as_complete();
+
+ /* Act. */
+ $progress_validation->run();
+
+ /* Assert. */
+ $this->assertSame(
+ 'Data mismatch between comments and tables based progress.',
+ $this->get_first_error_message( $progress_validation )
+ );
+ }
+
+ public function testRun_WhenHasQuizProgressInTable_HasNoErrors(): void {
+ /* Arrange. */
+ $course_data = $this->factory->get_course_with_lessons();
+ $lesson_id = $course_data['lesson_ids'][0];
+ $quiz_id = $course_data['quiz_ids'][0];
+ $progress_validation = new Progress_Validation();
+
+ add_filter( 'sensei_is_enrolled', '__return_true' );
+ $answers = $this->factory->generate_user_quiz_answers( $quiz_id );
+ Sensei_Quiz::save_user_answers( $answers, [], $lesson_id, 1 );
+ Sensei()->quiz->maybe_create_quiz_progress( $quiz_id, 1 );
+ $this->mask_migration_as_complete();
+
+ /* Act. */
+ $progress_validation->run();
+
+ /* Assert. */
+ $this->assertFalse( $progress_validation->has_errors() );
+ }
+
+ public function testRun_WhenHasNoQuizProgressInTable_HasError(): void {
+ /* Arrange. */
+ global $wpdb;
+ $course_data = $this->factory->get_course_with_lessons();
+ $lesson_id = $course_data['lesson_ids'][0];
+ $quiz_id = $course_data['quiz_ids'][0];
+ $progress_validation = new Progress_Validation();
+ $progress_repository = new Tables_Based_Quiz_Progress_Repository( $wpdb );
+
+ add_filter( 'sensei_is_enrolled', '__return_true' );
+ $answers = $this->factory->generate_user_quiz_answers( $quiz_id );
+ Sensei_Quiz::save_user_answers( $answers, [], $lesson_id, 1 );
+ Sensei()->quiz->maybe_create_quiz_progress( $quiz_id, 1 );
+ $progress_repository->delete_for_quiz( $quiz_id );
+ $this->mask_migration_as_complete();
+
+ /* Act. */
+ $progress_validation->run();
+
+ /* Assert. */
+ $this->assertSame(
+ 'Quiz tables based progress not found.',
+ $this->get_first_error_message( $progress_validation )
+ );
+ }
+
+ public function testRun_WhenHasQuizProgressButDataIsNotMatching_HasError(): void {
+ /* Arrange. */
+ global $wpdb;
+ $course_data = $this->factory->get_course_with_lessons();
+ $lesson_id = $course_data['lesson_ids'][0];
+ $quiz_id = $course_data['quiz_ids'][0];
+ $progress_validation = new Progress_Validation();
+ $progress_repository = new Tables_Based_Quiz_Progress_Repository( $wpdb );
+
+ add_filter( 'sensei_is_enrolled', '__return_true' );
+ $answers = $this->factory->generate_user_quiz_answers( $quiz_id );
+ Sensei_Quiz::save_user_answers( $answers, [], $lesson_id, 1 );
+ Sensei()->quiz->maybe_create_quiz_progress( $quiz_id, 1 );
+ $progress = $progress_repository->get( $quiz_id, 1 );
+ $progress->pass();
+ $progress_repository->save( $progress );
+ $this->mask_migration_as_complete();
+
+ /* Act. */
+ $progress_validation->run();
+
+ /* Assert. */
+ $this->assertSame(
+ 'Data mismatch between comments and tables based progress.',
+ $this->get_first_error_message( $progress_validation )
+ );
+ }
+
+ private function mask_migration_as_complete(): void {
+ update_option( Migration_Job_Scheduler::COMPLETED_OPTION_NAME, microtime( true ) );
+ }
+
+ private function get_first_error_message( Progress_Validation $progress_validation ): ?string {
+ $errors = $progress_validation->get_errors();
+
+ if ( empty( $errors ) ) {
+ return null;
+ }
+
+ return $errors[0]->get_message();
+ }
+}