diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml similarity index 100% rename from .github/workflows/ci.yml rename to .github/workflows/ci.yml diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index 06d6491..fe883dd 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -41,7 +41,7 @@ class provider implements */ public static function get_metadata($collection): \core_privacy\local\metadata\collection { $collection->add_database_table( - 'tool_pdfdetect_assigns', + 'tool_corruptpdfdetector_assigns', [ 'email' => 'privacy:metadata:tool_corruppdfdetector:email', 'userfullname' => 'privacy:metadata:tool_corruppdfdetector:userfullname', diff --git a/classes/table/assignments.php b/classes/table/assignments.php index 387e209..cf07135 100644 --- a/classes/table/assignments.php +++ b/classes/table/assignments.php @@ -31,7 +31,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class assignments extends html_table { - /** * Constructor */ @@ -89,7 +88,7 @@ public function __construct() { private function get_detected_assignments() { global $DB; - $records = $DB->get_records('tool_pdfdetect_assigns', [], 'detected ASC'); + $records = $DB->get_records('tool_corruptpdfdetector_assigns', [], 'detected ASC'); return $records; } @@ -106,12 +105,16 @@ public function get_percentage() { global $DB; $submissioncount = $DB->count_records('assign_submission'); - $filecount = $DB->count_records_select('tool_pdfdetect_assigns', - 'fixed = ?', [false], 'COUNT(DISTINCT submissionid)'); + $filecount = $DB->count_records_select( + 'tool_corruptpdfdetector_assigns', + 'fixed = ?', + [false], + 'COUNT(DISTINCT submissionid)' + ); if ($filecount === 0 || $submissioncount === 0) { return '0'; } - return round ($filecount * 100 / $submissioncount).'%'; + return round($filecount * 100 / $submissioncount) . '%'; } } diff --git a/classes/task/fix_assignments.php b/classes/task/fix_assignments.php index fc57779..d8e57ef 100644 --- a/classes/task/fix_assignments.php +++ b/classes/task/fix_assignments.php @@ -25,7 +25,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class fix_assignments extends \core\task\scheduled_task { - /** * {@inheritDoc} * @see \core\task\scheduled_task::get_name() @@ -42,19 +41,20 @@ public function execute() { global $DB; // Fetch all broken assignment submissions. - $records = $DB->get_recordset('tool_pdfdetect_assigns', ['fixed' => false]); + $records = $DB->get_recordset('tool_corruptpdfdetector_assigns', ['fixed' => false]); foreach ($records as $submission) { $contenthash = $submission->filename; $params['contenthash'] = $contenthash; - $DB->delete_records_select('files', + $DB->delete_records_select( + 'files', "contenthash = :contenthash AND filename = 'combined.pdf' AND (filearea = 'combined' OR filearea = 'partial')", - $params); + $params + ); $submission->fixed = true; - $DB->update_record('tool_pdfdetect_assigns', $submission); + $DB->update_record('tool_corruptpdfdetector_assigns', $submission); } $records->close(); } - } diff --git a/classes/task/scan_assignments.php b/classes/task/scan_assignments.php index 7a5b060..a4f78d9 100644 --- a/classes/task/scan_assignments.php +++ b/classes/task/scan_assignments.php @@ -25,9 +25,6 @@ require_once($CFG->dirroot . '/mod/assign/locallib.php'); require_once($CFG->dirroot . '/mod/assign/feedback/editpdf/fpdi/autoload.php'); -define("ONE", 1); -define("NUMBER_OF_EACH_RUN", 1000); - /** * Task to scan assignments. * @@ -37,7 +34,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class scan_assignments extends \core\task\scheduled_task { - /** * {@inheritDoc} * @see \core\task\scheduled_task::get_name() @@ -55,9 +51,9 @@ public function execute() { // Get the last submission id that had been checked in last run. $lastrun = $DB->get_records_sql('SELECT lastsubmissionid - FROM {tool_pdfdetect_runs} + FROM {tool_corruptpdfdetector_runs} ORDER BY runtime - DESC LIMIT :one', ['one' => ONE]); + DESC LIMIT 1'); $lastsubmitid = 0; if ($lastrun) { @@ -69,7 +65,7 @@ public function execute() { WHERE userid > 0 AND id > :id ORDER BY timecreated - ASC LIMIT :num', ['id' => $lastsubmitid, 'num' => NUMBER_OF_EACH_RUN]); + ASC', ['id' => $lastsubmitid], 0, 1000); $detectednum = 0; $run = new \stdClass(); @@ -79,7 +75,7 @@ public function execute() { $detectedsubmission = $this->detected_submission($submission); if ($detectedsubmission != null) { $pdfwitherror = $this->check_submission_combined_pdf($submission); - $detected = $DB->get_record('tool_pdfdetect_assigns', ['submissionid' => $submission->id]); + $detected = $DB->get_record('tool_corruptpdfdetector_assigns', ['submissionid' => $submission->id]); $detected->submitted = $submission->timemodified; if ($pdfwitherror != null) { $detected->detected = $pdfwitherror->detected; @@ -90,12 +86,12 @@ public function execute() { } else { $detected->fixed = true; } - $DB->update_record('tool_pdfdetect_assigns', $detected); + $DB->update_record('tool_corruptpdfdetector_assigns', $detected); } else { $pdfwitherror = $this->check_submission_combined_pdf($submission); if ($pdfwitherror != null) { $detectednum++; - $DB->insert_record('tool_pdfdetect_assigns', $pdfwitherror); + $DB->insert_record('tool_corruptpdfdetector_assigns', $pdfwitherror); } } } @@ -105,7 +101,7 @@ public function execute() { } $run->runtime = time(); $run->detectednumber = $detectednum; - $DB->insert_record('tool_pdfdetect_runs', $run); + $DB->insert_record('tool_corruptpdfdetector_runs', $run); } /** @@ -137,7 +133,7 @@ private function get_pdf_file_for_assignment(assign $assignment, stdClass $submi } /** - * Detects a previously recorded submission in the 'tool_pdfdetect_assigns' table. + * Detects a previously recorded submission in the 'tool_corruptpdfdetector_assigns' table. * * @param stdClass $submission The submission object containing the ID to look up in the database. * @@ -148,7 +144,7 @@ private function detected_submission(stdClass $submission): ?stdClass { $params = ['submissionid' => $submission->id]; $select = $DB->sql_compare_text('submissionid') . ' = ' . $DB->sql_compare_text(':submissionid'); - $detectedsubmission = $DB->get_record_select('tool_pdfdetect_assigns', $select, $params); + $detectedsubmission = $DB->get_record_select('tool_corruptpdfdetector_assigns', $select, $params); if (!empty($detectedsubmission)) { return $detectedsubmission; diff --git a/db/install.xml b/db/install.xml index 7050261..fac6d1a 100755 --- a/db/install.xml +++ b/db/install.xml @@ -4,7 +4,7 @@ xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd" > - +
@@ -27,7 +27,7 @@
- +
diff --git a/db/upgrade.php b/db/upgrade.php new file mode 100644 index 0000000..11d9bc5 --- /dev/null +++ b/db/upgrade.php @@ -0,0 +1,58 @@ +. + +/** + * Upgrade logic. + * + * @package tool_corruptpdfdetector + * @copyright Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Performs data migrations and updates on upgrade. + * + * @param integer $oldversion + * @return boolean + */ +function xmldb_tool_corruptpdfdetector_upgrade($oldversion = 0) { + global $CFG, $DB; + + require_once($CFG->libdir . '/db/upgradelib.php'); // Core Upgrade-related functions. + + $dbman = $DB->get_manager(); // Loads ddl manager and xmldb classes. + + if ($oldversion < 2026042301) { + // Rename table tool_pdfdetect_assigns to tool_corruptpdfdetector_assigns. + $table = new xmldb_table('tool_pdfdetect_assigns'); + + if ($dbman->table_exists($table)) { + $dbman->rename_table($table, 'tool_corruptpdfdetector_assigns'); + } + + // Rename table tool_pdfdetect_runs to tool_corruptpdfdetector_runs. + $table = new xmldb_table('tool_pdfdetect_runs'); + + if ($dbman->table_exists($table)) { + $dbman->rename_table($table, 'tool_corruptpdfdetector_runs'); + } + + // Corruptpdfdetector savepoint reached. + upgrade_plugin_savepoint(true, 2026042301, 'tool', 'corruptpdfdetector'); + } + + return true; +} diff --git a/index.php b/index.php index 1dc718c..d6170cf 100644 --- a/index.php +++ b/index.php @@ -29,7 +29,7 @@ require_capability('moodle/site:config', context_system::instance()); admin_externalpage_setup('tool_corruptpdfdetector'); -$assignments = new tool_corruptpdfdetector\table\assignments; +$assignments = new tool_corruptpdfdetector\table\assignments(); echo $OUTPUT->header(); echo html_writer::tag('h1', get_string('h1_current', 'tool_corruptpdfdetector')); diff --git a/settings.php b/settings.php index 8b16625..37e7d7d 100644 --- a/settings.php +++ b/settings.php @@ -27,6 +27,9 @@ if ($hassiteconfig) { $url = new moodle_url("/admin/tool/corruptpdfdetector"); - $ADMIN->add('server', new admin_externalpage('tool_corruptpdfdetector', - get_string('pluginname', 'tool_corruptpdfdetector'), $url)); + $ADMIN->add('server', new admin_externalpage( + 'tool_corruptpdfdetector', + get_string('pluginname', 'tool_corruptpdfdetector'), + $url + )); } diff --git a/tests/task_fix_assignments_test.php b/tests/task_fix_assignments_test.php new file mode 100644 index 0000000..f1d131a --- /dev/null +++ b/tests/task_fix_assignments_test.php @@ -0,0 +1,290 @@ +. + +namespace tool_corruptpdfdetector; + +use tool_corruptpdfdetector\task\fix_assignments; +/** + * Unit tests for the fix_assignments scheduled task. + * + * @covers \tool_corruptpdfdetector\task\fix_assignments + * @package tool_corruptpdfdetector + * @copyright Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +final class task_fix_assignments_test extends \advanced_testcase { + /** + * Helper: insert a minimal record into tool_corruptpdfdetector_assigns. + * + * @param string $contenthash SHA-1 hash stored in the filename column. + * @param bool $fixed Whether the record is already fixed. + * @return int Inserted record ID. + */ + private function insert_assigns_record(string $contenthash, bool $fixed = false): int { + global $DB; + + $record = (object)[ + 'assignid' => 1, + 'submissionid' => random_int(1, 99999), + 'coursename' => 'Test course', + 'assignname' => 'Test assignment', + 'userfullname' => 'Test User', + 'email' => 'test@example.com', + 'filename' => $contenthash, + 'message' => 'Corrupt PDF detected', + 'submitted' => time(), + 'detected' => time(), + 'fixed' => (int) $fixed, + ]; + + return $DB->insert_record('tool_corruptpdfdetector_assigns', $record); + } + + /** + * Helper: insert a minimal record into the files table with filename = 'combined.pdf'. + * + * @param string $contenthash Content hash matching the filename column in assigns. + * @param string $filearea 'combined' or 'partial'. + * @return int Inserted record ID. + */ + private function insert_file_record(string $contenthash, string $filearea = 'combined'): int { + global $DB; + + $record = (object)[ + 'contenthash' => $contenthash, + 'pathnamehash' => sha1(uniqid('', true)), + 'contextid' => \context_system::instance()->id, + 'component' => 'assignfeedback_editpdf', + 'filearea' => $filearea, + 'itemid' => 1, + 'filepath' => '/', + 'filename' => 'combined.pdf', + 'userid' => 0, + 'filesize' => 12345, + 'mimetype' => 'application/pdf', + 'status' => 0, + 'source' => null, + 'author' => null, + 'license' => null, + 'timecreated' => time(), + 'timemodified' => time(), + 'sortorder' => 0, + 'referencefileid' => null, + ]; + + return $DB->insert_record('files', $record); + } + + /** + * Verify that get_name() returns the expected localised string. + */ + public function test_task_name(): void { + $task = new fix_assignments(); + $this->assertEquals( + get_string('task_fix_assignments', 'tool_corruptpdfdetector'), + $task->get_name() + ); + } + + /** + * execute() should complete silently when there are no unfixed records. + */ + public function test_execute_does_nothing_when_table_is_empty(): void { + global $DB; + $this->resetAfterTest(); + + $task = new fix_assignments(); + $task->execute(); + + $this->assertEquals(0, $DB->count_records('tool_corruptpdfdetector_assigns')); + } + + /** + * execute() should mark every unfixed record as fixed after running. + */ + public function test_execute_marks_unfixed_records_as_fixed(): void { + global $DB; + $this->resetAfterTest(); + + $id1 = $this->insert_assigns_record(sha1('content1'), false); + $id2 = $this->insert_assigns_record(sha1('content2'), false); + + $task = new fix_assignments(); + $task->execute(); + + $record1 = $DB->get_record('tool_corruptpdfdetector_assigns', ['id' => $id1]); + $record2 = $DB->get_record('tool_corruptpdfdetector_assigns', ['id' => $id2]); + + $this->assertEquals(1, $record1->fixed, 'First record should be marked fixed.'); + $this->assertEquals(1, $record2->fixed, 'Second record should be marked fixed.'); + } + + /** + * execute() should delete the combined.pdf file (filearea = 'combined') for each unfixed record. + */ + public function test_execute_deletes_combined_pdf_file(): void { + global $DB; + $this->resetAfterTest(); + + $contenthash = sha1('combinedcontent'); + $this->insert_assigns_record($contenthash, false); + $fileid = $this->insert_file_record($contenthash, 'combined'); + + $this->assertTrue( + $DB->record_exists('files', ['id' => $fileid]), + 'File record should exist before task runs.' + ); + + $task = new fix_assignments(); + $task->execute(); + + $this->assertFalse( + $DB->record_exists('files', ['id' => $fileid]), + 'Combined PDF file record should be deleted after task runs.' + ); + } + + /** + * execute() should also delete the partial combined.pdf file (filearea = 'partial'). + */ + public function test_execute_deletes_partial_pdf_file(): void { + global $DB; + $this->resetAfterTest(); + + $contenthash = sha1('partialcontent'); + $this->insert_assigns_record($contenthash, false); + $fileid = $this->insert_file_record($contenthash, 'partial'); + + $task = new fix_assignments(); + $task->execute(); + + $this->assertFalse( + $DB->record_exists('files', ['id' => $fileid]), + 'Partial PDF file record should be deleted after task runs.' + ); + } + + /** + * execute() must NOT delete files unrelated to the corrupt submission + * (different contenthash, or a different filename in the same filearea). + */ + public function test_execute_does_not_delete_unrelated_files(): void { + global $DB; + $this->resetAfterTest(); + + $contenthash = sha1('targetcontent'); + $otherhash = sha1('othercontent'); + $this->insert_assigns_record($contenthash, false); + + // File with a different contenthash – must be preserved. + $differenthashfileid = $this->insert_file_record($otherhash); + + // File with the right hash but a different filename – must be preserved. + $differentnamerecord = (object)[ + 'contenthash' => $contenthash, + 'pathnamehash' => sha1(uniqid('', true)), + 'contextid' => \context_system::instance()->id, + 'component' => 'assignfeedback_editpdf', + 'filearea' => 'combined', + 'itemid' => 1, + 'filepath' => '/', + 'filename' => 'other.pdf', + 'userid' => 0, + 'filesize' => 100, + 'mimetype' => 'application/pdf', + 'status' => 0, + 'source' => null, + 'author' => null, + 'license' => null, + 'timecreated' => time(), + 'timemodified' => time(), + 'sortorder' => 0, + 'referencefileid' => null, + ]; + $differentnamefileid = $DB->insert_record('files', $differentnamerecord); + + $task = new fix_assignments(); + $task->execute(); + + $this->assertTrue( + $DB->record_exists('files', ['id' => $differenthashfileid]), + 'File with a different hash should not be deleted.' + ); + $this->assertTrue( + $DB->record_exists('files', ['id' => $differentnamefileid]), + 'File with a different filename should not be deleted.' + ); + } + + /** + * execute() should leave already-fixed records (fixed = 1) completely untouched. + */ + public function test_execute_skips_already_fixed_records(): void { + global $DB; + $this->resetAfterTest(); + + $contenthash = sha1('fixedcontent'); + $fixedid = $this->insert_assigns_record($contenthash, true); + $fileid = $this->insert_file_record($contenthash); + + $task = new fix_assignments(); + $task->execute(); + + $this->assertTrue( + $DB->record_exists('files', ['id' => $fileid]), + 'File for an already-fixed record should not be deleted.' + ); + + $record = $DB->get_record('tool_corruptpdfdetector_assigns', ['id' => $fixedid]); + $this->assertEquals(1, $record->fixed); + } + + /** + * execute() processes unfixed records while leaving already-fixed records intact. + */ + public function test_execute_mixed_fixed_and_unfixed_records(): void { + global $DB; + $this->resetAfterTest(); + + $unfixedhash = sha1('unfixedcontent'); + $fixedhash = sha1('alreadyfixedcontent'); + + $unfixedid = $this->insert_assigns_record($unfixedhash); + $fixedid = $this->insert_assigns_record($fixedhash, true); + + $unfixedfileid = $this->insert_file_record($unfixedhash); + $fixedfileid = $this->insert_file_record($fixedhash); + + $task = new fix_assignments(); + $task->execute(); + + // Unfixed record – should now be fixed, its file deleted. + $unfixedrecord = $DB->get_record('tool_corruptpdfdetector_assigns', ['id' => $unfixedid]); + $this->assertEquals(1, $unfixedrecord->fixed, 'Unfixed record should now be marked fixed.'); + $this->assertFalse( + $DB->record_exists('files', ['id' => $unfixedfileid]), + 'File for the unfixed record should be deleted.' + ); + + // Already-fixed record – unchanged, file retained. + $fixedrecord = $DB->get_record('tool_corruptpdfdetector_assigns', ['id' => $fixedid]); + $this->assertEquals(1, $fixedrecord->fixed, 'Already-fixed record should remain fixed.'); + $this->assertTrue( + $DB->record_exists('files', ['id' => $fixedfileid]), + 'File for the already-fixed record should not be deleted.' + ); + } +} diff --git a/tests/task_scan_assignments_test.php b/tests/task_scan_assignments_test.php new file mode 100644 index 0000000..4cfd10b --- /dev/null +++ b/tests/task_scan_assignments_test.php @@ -0,0 +1,302 @@ +. + +namespace tool_corruptpdfdetector; + +use tool_corruptpdfdetector\task\scan_assignments; + +/** + * Unit tests for the scan_assignments scheduled task. + * These tests cover the database-interaction logic of the task without + * requiring real PDF files or a fully configured assignment grading workflow. + * The private detected_submission() method is exercised via reflection. + * + * @covers \tool_corruptpdfdetector\task\scan_assignments + * @package tool_corruptpdfdetector + * @copyright Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +final class task_scan_assignments_test extends \advanced_testcase { + /** + * Insert a row into tool_corruptpdfdetector_runs. + * + * @param int $lastsubmissionid Last submission id stored by the run. + * @param int|null $runtime UNIX timestamp; defaults to current time. + * + * @return int Inserted record ID. + */ + private function insert_run_record(int $lastsubmissionid, ?int $runtime = null): int { + global $DB; + + return $DB->insert_record('tool_corruptpdfdetector_runs', (object)[ + 'lastsubmissionid' => $lastsubmissionid, + 'runtime' => $runtime ?? time(), + 'detectednumber' => 0, + ]); + } + + /** + * Insert a row into tool_corruptpdfdetector_assigns. + * + * @param int $submissionid Submission id to store. + * @param string $contenthash Optional content hash; derived from submissionid if omitted. + * + * @return int Inserted record ID. + */ + private function insert_assigns_record(int $submissionid, string $contenthash = ''): int { + global $DB; + + return $DB->insert_record('tool_corruptpdfdetector_assigns', (object)[ + 'assignid' => 1, + 'submissionid' => $submissionid, + 'coursename' => 'Test course', + 'assignname' => 'Test assignment', + 'userfullname' => 'Test User', + 'email' => 'test@example.com', + 'filename' => $contenthash ?: sha1((string) $submissionid), + 'message' => 'Corrupt PDF detected', + 'submitted' => time(), + 'detected' => time(), + 'fixed' => 0, + ]); + } + + /** + * Invoke the private detected_submission() method via reflection. + * + * @param \tool_corruptpdfdetector\task\scan_assignments $task Task instance. + * @param \stdClass $submission Minimal object with an `id` property. + * @return \stdClass|null + */ + private function call_detected_submission( + scan_assignments $task, + \stdClass $submission + ): ?\stdClass { + $method = new \ReflectionMethod( + scan_assignments::class, + 'detected_submission' + ); + + $method->setAccessible(true); + return $method->invoke($task, $submission); + } + + /** + * Verify that get_name() returns the expected localised string. + */ + public function test_task_name(): void { + $task = new scan_assignments(); + $this->assertEquals( + get_string('task_scan_assignments', 'tool_corruptpdfdetector'), + $task->get_name() + ); + } + + /** + * When there are no assign_submission rows and no prior run, execute() must + * create exactly one run record with lastsubmissionid = 0. + */ + public function test_execute_with_no_submissions_creates_initial_run_record(): void { + global $DB; + $this->resetAfterTest(); + + $this->assertEquals(0, $DB->count_records('assign_submission')); + $this->assertEquals(0, $DB->count_records('tool_corruptpdfdetector_runs')); + + $task = new scan_assignments(); + $task->execute(); + + $runs = $DB->get_records('tool_corruptpdfdetector_runs'); + $this->assertCount(1, $runs, 'Exactly one run record should be created.'); + + $run = reset($runs); + $this->assertEquals( + 0, + $run->lastsubmissionid, + 'lastsubmissionid should be 0 when no submissions exist.' + ); + $this->assertEquals( + 0, + $run->detectednumber, + 'detectednumber should be 0 when no submissions were scanned.' + ); + $this->assertGreaterThan( + 0, + $run->runtime, + 'runtime should be populated with a UNIX timestamp.' + ); + } + + /** + * Each call to execute() must produce its own run record; multiple runs accumulate. + */ + public function test_execute_creates_a_new_run_record_on_each_invocation(): void { + global $DB; + $this->resetAfterTest(); + + $task = new scan_assignments(); + $task->execute(); + $task->execute(); + + $this->assertEquals( + 2, + $DB->count_records('tool_corruptpdfdetector_runs'), + 'Two executions should produce two run records.' + ); + } + + /** + * When all submissions have already been processed (last run id is higher than + * any current submission id), execute() must reset lastsubmissionid to 0. + */ + public function test_execute_resets_last_submission_id_when_all_processed(): void { + global $DB; + $this->resetAfterTest(); + + $this->insert_run_record(PHP_INT_MAX); + + $task = new scan_assignments(); + $task->execute(); + + $runs = $DB->get_records('tool_corruptpdfdetector_runs', [], 'id DESC', '*', 0, 1); + $latestrun = reset($runs); + + $this->assertEquals( + 0, + $latestrun->lastsubmissionid, + 'lastsubmissionid should be reset to 0 when no new submissions are found.' + ); + } + + /** + * When multiple prior run records exist, execute() must use the one with the + * most recent runtime (ORDER BY runtime DESC LIMIT 1) to determine the starting + * submission id. When that id is very high, no submissions are found and the + * next run resets to lastsubmissionid = 0. + */ + public function test_execute_selects_latest_run_by_runtime(): void { + global $DB; + $this->resetAfterTest(); + + $oldtime = time() - 7200; + $recenttime = time() - 60; + + $this->insert_run_record(10, $oldtime); + $this->insert_run_record(PHP_INT_MAX, $recenttime); + + $task = new scan_assignments(); + $task->execute(); + + $runs = $DB->get_records('tool_corruptpdfdetector_runs', [], 'id DESC', '*', 0, 1); + $latestrun = reset($runs); + + $this->assertEquals( + 0, + $latestrun->lastsubmissionid, + 'Should use the most recent run by runtime, resulting in a reset to 0.' + ); + } + + /** + * detected_submission() must return null for a submission id that has no matching + * row in tool_corruptpdfdetector_assigns. + */ + public function test_detected_submission_returns_null_for_unknown_submission(): void { + $this->resetAfterTest(); + + $task = new scan_assignments(); + $result = $this->call_detected_submission($task, (object)['id' => 99999]); + + $this->assertNull( + $result, + 'detected_submission() should return null when submission is not in the assigns table.' + ); + } + + /** + * detected_submission() must return the matching record when the submission id + * exists in tool_corruptpdfdetector_assigns. + */ + public function test_detected_submission_returns_record_for_known_submission(): void { + $this->resetAfterTest(); + + $submissionid = 12345; + $this->insert_assigns_record($submissionid); + + $task = new scan_assignments(); + $result = $this->call_detected_submission($task, (object)['id' => $submissionid]); + + $this->assertNotNull( + $result, + 'detected_submission() should return a record when the submission id is found.' + ); + $this->assertEquals( + $submissionid, + $result->submissionid, + 'Returned record should have the correct submissionid.' + ); + } + + /** + * detected_submission() must return null after the assigns record for a given + * submission has been deleted (ensures there is no stale in-process caching). + */ + public function test_detected_submission_returns_null_after_record_deleted(): void { + global $DB; + $this->resetAfterTest(); + + $submissionid = 54321; + $id = $this->insert_assigns_record($submissionid); + + $task = new scan_assignments(); + + // Verify the record is found before deletion. + $this->assertNotNull( + $this->call_detected_submission($task, (object)['id' => $submissionid]) + ); + + // Delete the record and verify it is no longer found. + $DB->delete_records('tool_corruptpdfdetector_assigns', ['id' => $id]); + + $this->assertNull( + $this->call_detected_submission($task, (object)['id' => $submissionid]), + 'detected_submission() should return null after the DB record is removed.' + ); + } + + /** + * detected_submission() must match on the submissionid column, not on the + * assigns table primary key id. + */ + public function test_detected_submission_matches_on_submissionid_not_primary_id(): void { + $this->resetAfterTest(); + + $this->insert_assigns_record(111); + + $task = new scan_assignments(); + + // Looking up submissionid = 111 should succeed. + $this->assertNotNull( + $this->call_detected_submission($task, (object)['id' => 111]) + ); + + // A different id that is not a submissionid in the table should return null. + $this->assertNull( + $this->call_detected_submission($task, (object)['id' => 222]), + 'detected_submission() must not confuse the primary key with submissionid.' + ); + } +} diff --git a/version.php b/version.php index 79c581a..9579ae8 100644 --- a/version.php +++ b/version.php @@ -26,8 +26,8 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'tool_corruptpdfdetector'; -$plugin->release = 2026042300; -$plugin->version = 2026042300; +$plugin->release = 2026042301; +$plugin->version = 2026042301; $plugin->requires = 2024100700; $plugin->supported = [405, 405]; $plugin->maturity = MATURITY_ALPHA;