fix assessment import and exams draft
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\App\Controllers\View;
|
||||
|
||||
use App\Controllers\View\ExamDraftController;
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use ReflectionClass;
|
||||
use ReflectionMethod;
|
||||
|
||||
final class ExamDraftControllerGroupingTest extends CIUnitTestCase
|
||||
{
|
||||
public function testLatestReviewedRevisionControlsStatusAndFinalFile(): void
|
||||
{
|
||||
$rows = [
|
||||
$this->examRow([
|
||||
'id' => 12,
|
||||
'status' => 'accepted',
|
||||
'review_revision' => 2,
|
||||
'acceptance_type' => 'minor_edits',
|
||||
'reviewed_at' => '2026-05-02 12:00:00',
|
||||
'final_file' => 'final-v2.docx',
|
||||
'final_filename' => 'Grade 3 Final.docx',
|
||||
'final_pdf_file' => 'final-v2.pdf',
|
||||
]),
|
||||
$this->examRow([
|
||||
'id' => 11,
|
||||
'status' => 'under review',
|
||||
'review_revision' => 1,
|
||||
'reviewed_at' => '2026-05-01 12:00:00',
|
||||
'final_file' => 'review-v1.docx',
|
||||
]),
|
||||
$this->examRow([
|
||||
'id' => 10,
|
||||
'status' => 'submitted',
|
||||
'review_revision' => 0,
|
||||
'teacher_file' => 'teacher-v1.docx',
|
||||
]),
|
||||
];
|
||||
|
||||
$grouped = $this->invoke('groupReviewRevisions', [$rows]);
|
||||
|
||||
$this->assertCount(1, $grouped);
|
||||
$this->assertSame('accepted', $grouped[0]['status']);
|
||||
$this->assertSame(2, $grouped[0]['review_revision']);
|
||||
$this->assertSame('minor_edits', $grouped[0]['acceptance_type']);
|
||||
$this->assertSame('final-v2.docx', $grouped[0]['final_file']);
|
||||
$this->assertSame('final-v2.pdf', $grouped[0]['final_pdf_file']);
|
||||
$this->assertCount(2, $grouped[0]['review_files']);
|
||||
}
|
||||
|
||||
public function testGradeArchiveCombinesLegacyCopiesAcrossSections(): void
|
||||
{
|
||||
$rows = [
|
||||
$this->examRow([
|
||||
'id' => 1,
|
||||
'class_id' => 3,
|
||||
'class_name' => '3',
|
||||
'class_section_id' => 30,
|
||||
'class_section_name' => '3',
|
||||
'school_year' => '2024-2025',
|
||||
'status' => 'legacy',
|
||||
'final_file' => 'grade-3-final.pdf',
|
||||
]),
|
||||
$this->examRow([
|
||||
'id' => 2,
|
||||
'class_id' => 3,
|
||||
'class_name' => '3',
|
||||
'class_section_id' => 31,
|
||||
'class_section_name' => '3-A',
|
||||
'school_year' => '2024-2025',
|
||||
'status' => 'legacy',
|
||||
'final_file' => 'grade-3-final.pdf',
|
||||
]),
|
||||
];
|
||||
|
||||
$groups = $this->invoke('groupArchivedExamsByGrade', [$rows]);
|
||||
|
||||
$this->assertArrayHasKey(3, $groups);
|
||||
$this->assertSame('Grade 3', $groups[3]['class_section_name']);
|
||||
$this->assertCount(1, $groups[3]['items']);
|
||||
$this->assertSame(['3', '3-A'], $groups[3]['items'][0]['class_section_names']);
|
||||
}
|
||||
|
||||
public function testSubmissionsAreScopedToYearWhileAcceptedRecordsAreAlsoArchived(): void
|
||||
{
|
||||
$rows = [
|
||||
$this->examRow(['id' => 1, 'school_year' => '2025-2026', 'status' => 'submitted']),
|
||||
$this->examRow(['id' => 2, 'school_year' => '2024-2025', 'status' => 'under review']),
|
||||
$this->examRow(['id' => 3, 'school_year' => '2025-2026', 'status' => 'accepted']),
|
||||
$this->examRow(['id' => 4, 'school_year' => '2024-2025', 'status' => 'legacy']),
|
||||
];
|
||||
|
||||
[$drafts, $archived] = $this->invoke('partitionExamDraftsByYear', [$rows, '2025-2026']);
|
||||
|
||||
$this->assertSame([1, 3], array_column($drafts, 'id'));
|
||||
$this->assertSame([3, 4], array_column($archived, 'id'));
|
||||
}
|
||||
|
||||
public function testFinalArchiveKeepsOnlyLatestAcceptedTeacherVersion(): void
|
||||
{
|
||||
$rows = [
|
||||
$this->examRow(['id' => 10, 'status' => 'accepted', 'version' => 1, 'review_revision' => 3]),
|
||||
$this->examRow(['id' => 11, 'status' => 'accepted', 'version' => 2, 'review_revision' => 1]),
|
||||
$this->examRow(['id' => 12, 'status' => 'legacy', 'version' => 1]),
|
||||
];
|
||||
|
||||
$archive = $this->invoke('latestFinalArchiveRows', [$rows]);
|
||||
|
||||
$this->assertSame([12, 11], array_column($archive, 'id'));
|
||||
}
|
||||
|
||||
public function testFinalArchiveLinksArePdfOnly(): void
|
||||
{
|
||||
$view = file_get_contents(ROOTPATH . 'app/Views/administrator/exam_drafts.php') ?: '';
|
||||
|
||||
$this->assertStringContainsString("$" . "archivePdfFile = trim((string) ($" . "item['final_pdf_file']", $view);
|
||||
$this->assertStringContainsString('View PDF', $view);
|
||||
$this->assertStringContainsString('Download PDF', $view);
|
||||
$this->assertStringContainsString('Upload replacement PDF', $view);
|
||||
$this->assertStringContainsString('automatic conversion is unavailable', $view);
|
||||
}
|
||||
|
||||
public function testLossyPhpWordPdfFallbackIsNotUsedForFinalArchive(): void
|
||||
{
|
||||
$controller = file_get_contents(ROOTPATH . 'app/Controllers/View/ExamDraftController.php') ?: '';
|
||||
|
||||
$this->assertStringNotContainsString('PDF_RENDERER_DOMPDF', $controller);
|
||||
$this->assertStringContainsString('EXAM_PDF_CONVERTER_BINARY', $controller);
|
||||
$this->assertStringContainsString('STIRLING_PDF_URL', $controller);
|
||||
$this->assertStringContainsString('/api/v1/convert/file/pdf', $controller);
|
||||
$this->assertStringContainsString('-env:UserInstallation=file://', $controller);
|
||||
$this->assertStringNotContainsString('Legacy exams must be uploaded as PDF', $controller);
|
||||
}
|
||||
|
||||
public function testGeneratedPdfBesideDocxIsRejectedAsLossy(): void
|
||||
{
|
||||
$directory = WRITEPATH . 'uploads/exams/finals';
|
||||
$base = 'conversion-safety-' . uniqid('', true);
|
||||
$pdf = $directory . '/' . $base . '.pdf';
|
||||
$docx = $directory . '/' . $base . '.docx';
|
||||
$archivePdf = $directory . '/' . $base . '.archive.pdf';
|
||||
file_put_contents($pdf, '%PDF unsafe');
|
||||
file_put_contents($docx, 'source');
|
||||
file_put_contents($archivePdf, '%PDF safe');
|
||||
|
||||
try {
|
||||
$this->assertFalse($this->invoke('isFormattingSafePdf', [basename($pdf)]));
|
||||
$this->assertTrue($this->invoke('isFormattingSafePdf', [basename($archivePdf)]));
|
||||
unlink($docx);
|
||||
$this->assertTrue($this->invoke('isFormattingSafePdf', [basename($pdf)]));
|
||||
} finally {
|
||||
foreach ([$pdf, $docx, $archivePdf] as $path) {
|
||||
if (is_file($path)) {
|
||||
unlink($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private function examRow(array $overrides): array
|
||||
{
|
||||
return array_merge([
|
||||
'id' => 1,
|
||||
'teacher_id' => 20,
|
||||
'class_section_id' => 30,
|
||||
'class_section_name' => '3',
|
||||
'semester' => 'Spring',
|
||||
'school_year' => '2025-2026',
|
||||
'exam_type' => 'Final Exam',
|
||||
'version' => 1,
|
||||
'review_revision' => 0,
|
||||
'status' => 'submitted',
|
||||
'final_file' => null,
|
||||
'final_filename' => null,
|
||||
'final_pdf_file' => null,
|
||||
], $overrides);
|
||||
}
|
||||
|
||||
private function invoke(string $method, array $arguments): mixed
|
||||
{
|
||||
$controller = (new ReflectionClass(ExamDraftController::class))->newInstanceWithoutConstructor();
|
||||
$reflection = new ReflectionMethod($controller, $method);
|
||||
$reflection->setAccessible(true);
|
||||
|
||||
return $reflection->invokeArgs($controller, $arguments);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user