add projet
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\Admin;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminNotificationApiTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function createAdmin(): User
|
||||
{
|
||||
return User::factory()->create([
|
||||
'role' => 'administrator',
|
||||
'firstname' => 'Super',
|
||||
'lastname' => 'Admin',
|
||||
'email' => 'superadmin@example.com',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function seedRoleDataForAdmins(): array
|
||||
{
|
||||
// roles + user_roles used by AdminNotificationService::fetchAdminNotificationUsers()
|
||||
DB::table('roles')->insert([
|
||||
['id' => 1, 'name' => 'administrator'],
|
||||
['id' => 2, 'name' => 'teacher'],
|
||||
]);
|
||||
|
||||
$adminA = User::factory()->create([
|
||||
'firstname' => 'Nour',
|
||||
'lastname' => 'Ali',
|
||||
'email' => 'nour@example.com',
|
||||
'role' => 'administrator',
|
||||
]);
|
||||
|
||||
$adminB = User::factory()->create([
|
||||
'firstname' => 'Sara',
|
||||
'lastname' => 'Omar',
|
||||
'email' => 'sara@example.com',
|
||||
'role' => 'administrator',
|
||||
]);
|
||||
|
||||
$teacher = User::factory()->create([
|
||||
'firstname' => 'Teacher',
|
||||
'lastname' => 'User',
|
||||
'email' => 'teacher@example.com',
|
||||
'role' => 'teacher',
|
||||
]);
|
||||
|
||||
DB::table('user_roles')->insert([
|
||||
['user_id' => $adminA->id, 'role_id' => 1, 'deleted_at' => null],
|
||||
['user_id' => $adminB->id, 'role_id' => 1, 'deleted_at' => null],
|
||||
['user_id' => $teacher->id, 'role_id' => 2, 'deleted_at' => null],
|
||||
]);
|
||||
|
||||
return [$adminA, $adminB, $teacher];
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function admin_can_get_notification_alerts_payload(): void
|
||||
{
|
||||
$actingAdmin = $this->createAdmin();
|
||||
$this->seedRoleDataForAdmins();
|
||||
|
||||
$response = $this->actingAs($actingAdmin, 'sanctum')
|
||||
->getJson('/api/admin/notifications/alerts');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonStructure([
|
||||
'admins',
|
||||
'subjects',
|
||||
'assigned_subjects',
|
||||
'table_ready',
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function admin_can_save_notification_subjects(): void
|
||||
{
|
||||
$actingAdmin = $this->createAdmin();
|
||||
[$adminA, $adminB] = $this->seedRoleDataForAdmins();
|
||||
|
||||
$payload = [
|
||||
'subjects' => [
|
||||
$adminA->id => [
|
||||
'finance' => true,
|
||||
'attendance' => true,
|
||||
],
|
||||
$adminB->id => [
|
||||
'general',
|
||||
'events',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$response = $this->actingAs($actingAdmin, 'sanctum')
|
||||
->postJson('/api/admin/notifications/subjects', $payload);
|
||||
|
||||
// If table is missing in your migrations, service returns 422 (ok=false)
|
||||
if ($response->status() === 422) {
|
||||
$response->assertJson([
|
||||
'ok' => false,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$response->assertOk()
|
||||
->assertJson([
|
||||
'ok' => true,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('admin_notification_subjects', [
|
||||
'admin_id' => $adminA->id,
|
||||
'subject' => 'finance',
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function admin_can_get_print_recipients_payload(): void
|
||||
{
|
||||
$actingAdmin = $this->createAdmin();
|
||||
$this->seedRoleDataForAdmins();
|
||||
|
||||
$response = $this->actingAs($actingAdmin, 'sanctum')
|
||||
->getJson('/api/admin/notifications/print-recipients');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonStructure([
|
||||
'admins',
|
||||
'assigned',
|
||||
'table_ready',
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function admin_can_save_print_recipients(): void
|
||||
{
|
||||
$actingAdmin = $this->createAdmin();
|
||||
[$adminA, $adminB] = $this->seedRoleDataForAdmins();
|
||||
|
||||
$payload = [
|
||||
'notify' => [
|
||||
(string)$adminA->id => true,
|
||||
(string)$adminB->id => true,
|
||||
],
|
||||
];
|
||||
|
||||
$response = $this->actingAs($actingAdmin, 'sanctum')
|
||||
->postJson('/api/admin/notifications/print-recipients', $payload);
|
||||
|
||||
if ($response->status() === 422) {
|
||||
$response->assertJson(['ok' => false]);
|
||||
return;
|
||||
}
|
||||
|
||||
$response->assertOk()
|
||||
->assertJson([
|
||||
'ok' => true,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('admin_notification_subjects', [
|
||||
'admin_id' => $adminA->id,
|
||||
'subject' => 'print_requests',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\Admin;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminProgressApiTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->createProgressTestTablesIfMissing();
|
||||
config()->set('progress.status_options', [
|
||||
'draft' => 'Draft',
|
||||
'submitted' => 'Submitted',
|
||||
'reviewed' => 'Reviewed',
|
||||
'approved' => 'Approved',
|
||||
'rejected' => 'Rejected',
|
||||
]);
|
||||
config()->set('progress.subject_sections', [
|
||||
'quran' => ['Reading', 'Memorization'],
|
||||
'arabic' => ['Reading', 'Writing'],
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_lists_grouped_progress_reports(): void
|
||||
{
|
||||
$sectionId = $this->insertClassSection('Grade 5 - A');
|
||||
$this->insertStudentClass($sectionId);
|
||||
|
||||
$this->insertProgressReport([
|
||||
'class_section_id' => $sectionId,
|
||||
'subject' => 'quran',
|
||||
'status' => 'submitted',
|
||||
'week_start' => '2026-02-09',
|
||||
'week_end' => '2026-02-15',
|
||||
]);
|
||||
|
||||
$this->insertProgressReport([
|
||||
'class_section_id' => $sectionId,
|
||||
'subject' => 'arabic',
|
||||
'status' => 'submitted',
|
||||
'week_start' => '2026-02-09',
|
||||
'week_end' => '2026-02-15',
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/admin/progress');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonStructure([
|
||||
'data' => [
|
||||
'*' => [
|
||||
'week_start',
|
||||
'week_end',
|
||||
'class_section_id',
|
||||
'class_section_name',
|
||||
'reports',
|
||||
],
|
||||
],
|
||||
'meta' => [
|
||||
'filters',
|
||||
'class_sections',
|
||||
'status_options',
|
||||
'subject_sections',
|
||||
],
|
||||
]);
|
||||
|
||||
$data = $response->json('data');
|
||||
$this->assertCount(1, $data);
|
||||
$this->assertEquals('2026-02-09', $data[0]['week_start']);
|
||||
$this->assertEquals($sectionId, $data[0]['class_section_id']);
|
||||
$this->assertArrayHasKey('quran', $data[0]['reports']);
|
||||
$this->assertArrayHasKey('arabic', $data[0]['reports']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_filters_progress_reports_by_status_and_class_section(): void
|
||||
{
|
||||
$sectionA = $this->insertClassSection('Grade 4 - A');
|
||||
$sectionB = $this->insertClassSection('Grade 6 - B');
|
||||
|
||||
$this->insertStudentClass($sectionA);
|
||||
$this->insertStudentClass($sectionB);
|
||||
|
||||
$this->insertProgressReport([
|
||||
'class_section_id' => $sectionA,
|
||||
'subject' => 'quran',
|
||||
'status' => 'approved',
|
||||
'week_start' => '2026-02-09',
|
||||
'week_end' => '2026-02-15',
|
||||
]);
|
||||
|
||||
$this->insertProgressReport([
|
||||
'class_section_id' => $sectionB,
|
||||
'subject' => 'arabic',
|
||||
'status' => 'draft',
|
||||
'week_start' => '2026-02-09',
|
||||
'week_end' => '2026-02-15',
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/admin/progress?status=approved&class_section_id=' . $sectionA);
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$data = $response->json('data');
|
||||
$this->assertCount(1, $data);
|
||||
$this->assertEquals($sectionA, $data[0]['class_section_id']);
|
||||
$this->assertArrayHasKey('quran', $data[0]['reports']);
|
||||
$this->assertArrayNotHasKey('arabic', $data[0]['reports']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_shows_progress_report_with_weekly_reports_and_attachments(): void
|
||||
{
|
||||
$sectionId = $this->insertClassSection('Grade 5 - A');
|
||||
$report1 = $this->insertProgressReport([
|
||||
'class_section_id' => $sectionId,
|
||||
'subject' => 'quran',
|
||||
'status' => 'submitted',
|
||||
'week_start' => '2026-02-09',
|
||||
'week_end' => '2026-02-15',
|
||||
'flags_json' => json_encode(['needs_followup' => true]),
|
||||
]);
|
||||
|
||||
$report2 = $this->insertProgressReport([
|
||||
'class_section_id' => $sectionId,
|
||||
'subject' => 'arabic',
|
||||
'status' => 'submitted',
|
||||
'week_start' => '2026-02-09',
|
||||
'week_end' => '2026-02-15',
|
||||
]);
|
||||
|
||||
$this->insertAttachment($report1, [
|
||||
'original_name' => 'quran-notes.pdf',
|
||||
'file_path' => 'uploads/progress/quran-notes.pdf',
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/admin/progress/reports/' . $report1);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonStructure([
|
||||
'data' => [
|
||||
'report',
|
||||
'weekly_reports',
|
||||
'subject_sections',
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals($report1, $response->json('data.report.id'));
|
||||
$this->assertEquals('Submitted', $response->json('data.report.status_label'));
|
||||
$this->assertTrue($response->json('data.report.flags.needs_followup'));
|
||||
|
||||
$weekly = $response->json('data.weekly_reports');
|
||||
$this->assertCount(2, $weekly);
|
||||
|
||||
$quranReport = collect($weekly)->firstWhere('subject', 'quran');
|
||||
$this->assertNotNull($quranReport);
|
||||
$this->assertNotEmpty($quranReport['attachments']);
|
||||
$this->assertEquals('quran-notes.pdf', $quranReport['attachments'][0]['name']);
|
||||
|
||||
$arabicReport = collect($weekly)->firstWhere('subject', 'arabic');
|
||||
$this->assertNotNull($arabicReport);
|
||||
$this->assertIsArray($arabicReport['attachments']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_returns_404_for_missing_progress_report(): void
|
||||
{
|
||||
$this->getJson('/api/admin/progress/reports/999999')
|
||||
->assertStatus(404)
|
||||
->assertJson([
|
||||
'message' => 'Progress report not found.',
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_downloads_legacy_attachment_from_report_attachment_path(): void
|
||||
{
|
||||
File::ensureDirectoryExists(storage_path('app/uploads/progress'));
|
||||
$fullPath = storage_path('app/uploads/progress/legacy-file.txt');
|
||||
File::put($fullPath, 'legacy attachment content');
|
||||
|
||||
$reportId = $this->insertProgressReport([
|
||||
'class_section_id' => 1,
|
||||
'subject' => 'quran',
|
||||
'status' => 'submitted',
|
||||
'week_start' => '2026-02-09',
|
||||
'week_end' => '2026-02-15',
|
||||
'attachment_path' => 'writable/uploads/progress/legacy-file.txt',
|
||||
]);
|
||||
|
||||
$response = $this->get('/api/admin/progress/reports/' . $reportId . '/attachment');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertHeader('content-disposition');
|
||||
$this->assertStringContainsString('legacy-file.txt', $response->headers->get('content-disposition', ''));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_returns_404_when_legacy_attachment_is_missing(): void
|
||||
{
|
||||
$reportId = $this->insertProgressReport([
|
||||
'class_section_id' => 1,
|
||||
'subject' => 'quran',
|
||||
'status' => 'submitted',
|
||||
'week_start' => '2026-02-09',
|
||||
'week_end' => '2026-02-15',
|
||||
'attachment_path' => 'writable/uploads/progress/missing.txt',
|
||||
]);
|
||||
|
||||
$this->get('/api/admin/progress/reports/' . $reportId . '/attachment')
|
||||
->assertStatus(404);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_downloads_normalized_attachment_by_attachment_id(): void
|
||||
{
|
||||
File::ensureDirectoryExists(storage_path('app/uploads/progress'));
|
||||
$fullPath = storage_path('app/uploads/progress/normalized-file.txt');
|
||||
File::put($fullPath, 'normalized attachment content');
|
||||
|
||||
$reportId = $this->insertProgressReport([
|
||||
'class_section_id' => 1,
|
||||
'subject' => 'arabic',
|
||||
'status' => 'approved',
|
||||
'week_start' => '2026-02-09',
|
||||
'week_end' => '2026-02-15',
|
||||
]);
|
||||
|
||||
$attachmentId = $this->insertAttachment($reportId, [
|
||||
'original_name' => 'Teacher Notes.txt',
|
||||
'file_path' => 'writable/uploads/progress/normalized-file.txt',
|
||||
]);
|
||||
|
||||
$response = $this->get('/api/admin/progress/attachments/' . $attachmentId);
|
||||
|
||||
$response->assertOk();
|
||||
$this->assertStringContainsString('Teacher Notes.txt', $response->headers->get('content-disposition', ''));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_returns_404_for_missing_normalized_attachment(): void
|
||||
{
|
||||
$this->get('/api/admin/progress/attachments/999999')
|
||||
->assertStatus(404);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private function createProgressTestTablesIfMissing(): void
|
||||
{
|
||||
if (!Schema::hasTable('classSection')) {
|
||||
Schema::create('classSection', function (Blueprint $table) {
|
||||
$table->increments('class_section_id');
|
||||
$table->string('class_section_name')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('student_class')) {
|
||||
Schema::create('student_class', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->unsignedInteger('class_section_id');
|
||||
$table->unsignedInteger('student_id')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('users')) {
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('firstname')->nullable();
|
||||
$table->string('lastname')->nullable();
|
||||
$table->string('email')->nullable();
|
||||
$table->string('password')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
} else {
|
||||
// Add columns if test app users table exists but lacks firstname/lastname
|
||||
if (!Schema::hasColumn('users', 'firstname')) {
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('firstname')->nullable();
|
||||
});
|
||||
}
|
||||
if (!Schema::hasColumn('users', 'lastname')) {
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('lastname')->nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('class_progress_reports')) {
|
||||
Schema::create('class_progress_reports', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedInteger('class_section_id')->nullable();
|
||||
$table->unsignedBigInteger('teacher_id')->nullable();
|
||||
$table->string('subject')->nullable();
|
||||
$table->date('week_start')->nullable();
|
||||
$table->date('week_end')->nullable();
|
||||
$table->string('status')->nullable();
|
||||
$table->text('flags_json')->nullable();
|
||||
$table->string('attachment_path')->nullable();
|
||||
$table->text('notes')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('class_progress_attachments')) {
|
||||
Schema::create('class_progress_attachments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('report_id');
|
||||
$table->string('original_name')->nullable();
|
||||
$table->string('file_path')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private function insertClassSection(string $name): int
|
||||
{
|
||||
return (int) \DB::table('classSection')->insertGetId([
|
||||
'class_section_name' => $name,
|
||||
], 'class_section_id');
|
||||
}
|
||||
|
||||
private function insertStudentClass(int $classSectionId): int
|
||||
{
|
||||
return (int) \DB::table('student_class')->insertGetId([
|
||||
'class_section_id' => $classSectionId,
|
||||
'student_id' => rand(1000, 9999),
|
||||
]);
|
||||
}
|
||||
|
||||
private function insertProgressReport(array $override = []): int
|
||||
{
|
||||
$data = array_merge([
|
||||
'class_section_id' => 1,
|
||||
'teacher_id' => null,
|
||||
'subject' => 'quran',
|
||||
'week_start' => '2026-02-09',
|
||||
'week_end' => '2026-02-15',
|
||||
'status' => 'draft',
|
||||
'flags_json' => null,
|
||||
'attachment_path' => null,
|
||||
'notes' => 'Test report',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
], $override);
|
||||
|
||||
return (int) \DB::table('class_progress_reports')->insertGetId($data);
|
||||
}
|
||||
|
||||
private function insertAttachment(int $reportId, array $override = []): int
|
||||
{
|
||||
$data = array_merge([
|
||||
'report_id' => $reportId,
|
||||
'original_name' => 'Attachment.pdf',
|
||||
'file_path' => 'writable/uploads/progress/attachment.pdf',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
], $override);
|
||||
|
||||
return (int) \DB::table('class_progress_attachments')->insertGetId($data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\Admin;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TeacherSubmissionApiTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function createAdmin(): User
|
||||
{
|
||||
$admin = User::factory()->create([
|
||||
'role' => 'administrator',
|
||||
'firstname' => 'Admin',
|
||||
'lastname' => 'User',
|
||||
'email' => 'admin@example.com',
|
||||
]);
|
||||
|
||||
return $admin;
|
||||
}
|
||||
|
||||
protected function seedSchoolConfig(): void
|
||||
{
|
||||
DB::table('configurations')->insert([
|
||||
['config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
['config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function admin_can_fetch_teacher_submission_report(): void
|
||||
{
|
||||
$admin = $this->createAdmin();
|
||||
$this->seedSchoolConfig();
|
||||
|
||||
// Minimal records to allow report query to run
|
||||
DB::table('classSection')->insert([
|
||||
'class_section_id' => 1,
|
||||
'class_section_name' => 'Grade 5 - A',
|
||||
]);
|
||||
|
||||
$teacher = User::factory()->create([
|
||||
'firstname' => 'Fatima',
|
||||
'lastname' => 'Teacher',
|
||||
'email' => 'teacher1@example.com',
|
||||
]);
|
||||
|
||||
DB::table('teacher_class')->insert([
|
||||
'teacher_id' => $teacher->id,
|
||||
'class_section_id' => 1,
|
||||
'position' => 'main',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($admin, 'sanctum')
|
||||
->getJson('/api/admin/teacher-submissions/report');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonStructure([
|
||||
'semester',
|
||||
'school_year',
|
||||
'rows',
|
||||
'notification_history',
|
||||
'summary' => [
|
||||
'total_items',
|
||||
'missing_items',
|
||||
'submitted_items',
|
||||
'submission_percentage',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function admin_can_send_teacher_submission_notifications(): void
|
||||
{
|
||||
Mail::fake();
|
||||
|
||||
$admin = $this->createAdmin();
|
||||
$this->seedSchoolConfig();
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'class_section_id' => 2,
|
||||
'class_section_name' => 'Grade 6 - B',
|
||||
]);
|
||||
|
||||
$teacher = User::factory()->create([
|
||||
'firstname' => 'Amina',
|
||||
'lastname' => 'Smith',
|
||||
'email' => 'amina@example.com',
|
||||
]);
|
||||
|
||||
// optional history table insert compatibility depends on schema; no need to seed rows
|
||||
$payload = [
|
||||
'notify' => [
|
||||
2 => [
|
||||
$teacher->id => 1,
|
||||
],
|
||||
],
|
||||
'missing_items' => [
|
||||
2 => [
|
||||
$teacher->id => base64_encode(json_encode(['midterm scores', 'PTAP comments'])),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$response = $this->actingAs($admin, 'sanctum')
|
||||
->postJson('/api/admin/teacher-submissions/notifications', $payload);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonStructure([
|
||||
'message',
|
||||
'sent',
|
||||
'failed',
|
||||
'results' => [
|
||||
'*' => [
|
||||
'teacher_id',
|
||||
'class_section_id',
|
||||
'teacher_name',
|
||||
'section_name',
|
||||
'email',
|
||||
'status',
|
||||
'missing_items',
|
||||
'error',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
Mail::assertSentCount(1);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function teacher_submission_notifications_requires_notify_payload(): void
|
||||
{
|
||||
$admin = $this->createAdmin();
|
||||
|
||||
$response = $this->actingAs($admin, 'sanctum')
|
||||
->postJson('/api/admin/teacher-submissions/notifications', []);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['notify']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function non_admin_cannot_access_teacher_submission_endpoints(): void
|
||||
{
|
||||
$user = User::factory()->create(['role' => 'teacher']);
|
||||
|
||||
$this->actingAs($user, 'sanctum')
|
||||
->getJson('/api/admin/teacher-submissions/report')
|
||||
->assertStatus(403);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Student;
|
||||
use App\Models\StudentClass;
|
||||
use App\Models\TeacherClass;
|
||||
use App\Models\ClassSection;
|
||||
use App\Models\Configuration;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AssignmentApiTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $user;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// If your app uses Sanctum auth
|
||||
$this->user = User::factory()->create();
|
||||
Sanctum::actingAs($this->user);
|
||||
|
||||
// Seed config values used by AssignmentService
|
||||
// Adjust columns if your configuration table schema differs
|
||||
Configuration::query()->updateOrCreate(
|
||||
['key' => 'semester'],
|
||||
['value' => 'Fall']
|
||||
);
|
||||
|
||||
Configuration::query()->updateOrCreate(
|
||||
['key' => 'school_year'],
|
||||
['value' => '2025-2026']
|
||||
);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_returns_assignment_sections_json(): void
|
||||
{
|
||||
$section = ClassSection::factory()->create([
|
||||
'name' => 'Grade 5 - A', // or title depending on your schema
|
||||
]);
|
||||
|
||||
$teacher = User::factory()->create([
|
||||
'firstname' => 'Ahmad',
|
||||
'lastname' => 'Ali',
|
||||
]);
|
||||
|
||||
TeacherClass::query()->create([
|
||||
'teacher_id' => $teacher->id,
|
||||
'class_section_id' => $section->id,
|
||||
'position' => 'main',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'description' => 'Main teacher assignment',
|
||||
]);
|
||||
|
||||
$student = Student::factory()->create([
|
||||
'firstname' => 'Sara',
|
||||
'lastname' => 'Yousef',
|
||||
'is_active' => 1,
|
||||
'gender' => 'F',
|
||||
'registration_grade' => '5',
|
||||
'photo_consent' => 1,
|
||||
'tuition_paid' => 1,
|
||||
'school_id' => 'SCH-1001',
|
||||
]);
|
||||
|
||||
StudentClass::query()->create([
|
||||
'student_id' => $student->id,
|
||||
'class_section_id' => $section->id,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'description' => 'Student assigned',
|
||||
'is_active' => 1, // remove if your table doesn’t have this column
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/assignments');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonStructure([
|
||||
'classSections' => [
|
||||
'*' => [
|
||||
'class_section_id',
|
||||
'class_section_name',
|
||||
'main_teachers',
|
||||
'teacher_assistants',
|
||||
'students',
|
||||
'semester',
|
||||
'school_year',
|
||||
'description',
|
||||
]
|
||||
],
|
||||
'schoolYears',
|
||||
'selectedYear',
|
||||
'selectedSemester',
|
||||
'semester',
|
||||
'school_year',
|
||||
]);
|
||||
|
||||
$response->assertJsonPath('classSections.0.class_section_id', $section->id);
|
||||
$response->assertJsonPath('classSections.0.main_teachers.0', 'Ahmad Ali');
|
||||
$response->assertJsonPath('classSections.0.students.0.id', $student->id);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_filters_assignments_by_school_year(): void
|
||||
{
|
||||
$section1 = ClassSection::factory()->create(['name' => 'Grade 4']);
|
||||
$section2 = ClassSection::factory()->create(['name' => 'Grade 6']);
|
||||
|
||||
$teacher = User::factory()->create(['firstname' => 'Test', 'lastname' => 'Teacher']);
|
||||
|
||||
TeacherClass::query()->create([
|
||||
'teacher_id' => $teacher->id,
|
||||
'class_section_id' => $section1->id,
|
||||
'position' => 'main',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2024-2025',
|
||||
]);
|
||||
|
||||
TeacherClass::query()->create([
|
||||
'teacher_id' => $teacher->id,
|
||||
'class_section_id' => $section2->id,
|
||||
'position' => 'main',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/assignments?school_year=2025-2026');
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$sections = $response->json('classSections');
|
||||
$ids = collect($sections)->pluck('class_section_id')->all();
|
||||
|
||||
$this->assertContains($section2->id, $ids);
|
||||
$this->assertNotContains($section1->id, $ids);
|
||||
$response->assertJsonPath('selectedYear', '2025-2026');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_creates_or_updates_student_assignment(): void
|
||||
{
|
||||
$student = Student::factory()->create(['is_active' => 1]);
|
||||
$section = ClassSection::factory()->create();
|
||||
|
||||
$payload = [
|
||||
'student_id' => $student->id,
|
||||
'class_section_id' => $section->id,
|
||||
'semester' => 'Spring',
|
||||
'school_year' => '2025-2026',
|
||||
'description' => 'Moved after placement review',
|
||||
];
|
||||
|
||||
$response = $this->postJson('/api/assignments', $payload);
|
||||
|
||||
$response->assertCreated()
|
||||
->assertJsonPath('message', 'Assignment saved successfully');
|
||||
|
||||
$this->assertDatabaseHas('student_class', [
|
||||
'student_id' => $student->id,
|
||||
'class_section_id' => $section->id,
|
||||
'semester' => 'Spring',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_validates_required_fields_when_storing_assignment(): void
|
||||
{
|
||||
$response = $this->postJson('/api/assignments', []);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors([
|
||||
'student_id',
|
||||
'class_section_id',
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function store_is_idempotent_for_same_student_semester_and_year(): void
|
||||
{
|
||||
$student = Student::factory()->create(['is_active' => 1]);
|
||||
$sectionA = ClassSection::factory()->create();
|
||||
$sectionB = ClassSection::factory()->create();
|
||||
|
||||
// First create
|
||||
$this->postJson('/api/assignments', [
|
||||
'student_id' => $student->id,
|
||||
'class_section_id' => $sectionA->id,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
])->assertCreated();
|
||||
|
||||
// Second call same unique key should update (per service updateOrCreate)
|
||||
$this->postJson('/api/assignments', [
|
||||
'student_id' => $student->id,
|
||||
'class_section_id' => $sectionB->id,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'description' => 'Reassigned',
|
||||
])->assertCreated();
|
||||
|
||||
$this->assertEquals(
|
||||
1,
|
||||
StudentClass::query()
|
||||
->where('student_id', $student->id)
|
||||
->where('semester', 'Fall')
|
||||
->where('school_year', '2025-2026')
|
||||
->count()
|
||||
);
|
||||
|
||||
$this->assertDatabaseHas('student_class', [
|
||||
'student_id' => $student->id,
|
||||
'class_section_id' => $sectionB->id,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function guest_cannot_access_assignment_api_when_auth_is_required(): void
|
||||
{
|
||||
auth()->guard('sanctum')->logout();
|
||||
|
||||
$response = $this->getJson('/api/assignments');
|
||||
|
||||
$response->assertStatus(401);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api;
|
||||
|
||||
use App\Services\AttendanceCommentTemplateService;
|
||||
use Illuminate\Foundation\Testing\WithFaker;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AttendanceCommentTemplateApiTest extends TestCase
|
||||
{
|
||||
use WithFaker;
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Mockery::close();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_can_list_templates(): void
|
||||
{
|
||||
$mock = Mockery::mock(AttendanceCommentTemplateService::class);
|
||||
$mock->shouldReceive('list')
|
||||
->once()
|
||||
->with(false)
|
||||
->andReturn([
|
||||
[
|
||||
'id' => 1,
|
||||
'min_score' => 0,
|
||||
'max_score' => 59,
|
||||
'template_text' => 'Needs improvement',
|
||||
'is_active' => true,
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'min_score' => 60,
|
||||
'max_score' => 79,
|
||||
'template_text' => 'Good effort',
|
||||
'is_active' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->app->instance(AttendanceCommentTemplateService::class, $mock);
|
||||
|
||||
$response = $this->getJson('/api/attendance-comment-templates');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJson([
|
||||
'status' => 'success',
|
||||
])
|
||||
->assertJsonCount(2, 'data')
|
||||
->assertJsonPath('data.0.id', 1)
|
||||
->assertJsonPath('data.1.max_score', 79);
|
||||
}
|
||||
|
||||
public function test_can_list_templates_active_only(): void
|
||||
{
|
||||
$mock = Mockery::mock(AttendanceCommentTemplateService::class);
|
||||
$mock->shouldReceive('list')
|
||||
->once()
|
||||
->with(true)
|
||||
->andReturn([]);
|
||||
|
||||
$this->app->instance(AttendanceCommentTemplateService::class, $mock);
|
||||
|
||||
$response = $this->getJson('/api/attendance-comment-templates?active_only=1');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJson([
|
||||
'status' => 'success',
|
||||
'data' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_can_create_template(): void
|
||||
{
|
||||
$payload = [
|
||||
'min_score' => 80,
|
||||
'max_score' => 100,
|
||||
'template_text' => 'Excellent attendance and punctuality.',
|
||||
'is_active' => true,
|
||||
];
|
||||
|
||||
$mock = Mockery::mock(AttendanceCommentTemplateService::class);
|
||||
$mock->shouldReceive('create')
|
||||
->once()
|
||||
->with(Mockery::on(function ($data) use ($payload) {
|
||||
return (int)$data['min_score'] === 80
|
||||
&& (int)$data['max_score'] === 100
|
||||
&& $data['template_text'] === $payload['template_text']
|
||||
&& (bool)$data['is_active'] === true;
|
||||
}))
|
||||
->andReturn([
|
||||
'id' => 10,
|
||||
'min_score' => 80,
|
||||
'max_score' => 100,
|
||||
'template_text' => 'Excellent attendance and punctuality.',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->app->instance(AttendanceCommentTemplateService::class, $mock);
|
||||
|
||||
$response = $this->postJson('/api/attendance-comment-templates', $payload);
|
||||
|
||||
$response->assertCreated()
|
||||
->assertJsonPath('status', 'success')
|
||||
->assertJsonPath('data.id', 10)
|
||||
->assertJsonPath('data.min_score', 80)
|
||||
->assertJsonPath('data.max_score', 100);
|
||||
}
|
||||
|
||||
public function test_store_validation_fails_when_max_less_than_min(): void
|
||||
{
|
||||
$payload = [
|
||||
'min_score' => 90,
|
||||
'max_score' => 70,
|
||||
'template_text' => 'Invalid range',
|
||||
'is_active' => true,
|
||||
];
|
||||
|
||||
$response = $this->postJson('/api/attendance-comment-templates', $payload);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['max_score']);
|
||||
}
|
||||
|
||||
public function test_can_show_template(): void
|
||||
{
|
||||
$mock = Mockery::mock(AttendanceCommentTemplateService::class);
|
||||
$mock->shouldReceive('findOrFail')
|
||||
->once()
|
||||
->with(5)
|
||||
->andReturn([
|
||||
'id' => 5,
|
||||
'min_score' => 60,
|
||||
'max_score' => 69,
|
||||
'template_text' => 'Average attendance',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->app->instance(AttendanceCommentTemplateService::class, $mock);
|
||||
|
||||
$response = $this->getJson('/api/attendance-comment-templates/5');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('status', 'success')
|
||||
->assertJsonPath('data.id', 5);
|
||||
}
|
||||
|
||||
public function test_can_update_template(): void
|
||||
{
|
||||
$payload = [
|
||||
'template_text' => 'Updated comment text',
|
||||
'is_active' => false,
|
||||
];
|
||||
|
||||
$mock = Mockery::mock(AttendanceCommentTemplateService::class);
|
||||
$mock->shouldReceive('update')
|
||||
->once()
|
||||
->with(7, Mockery::on(function ($data) {
|
||||
return $data['template_text'] === 'Updated comment text'
|
||||
&& (bool)$data['is_active'] === false;
|
||||
}))
|
||||
->andReturn([
|
||||
'id' => 7,
|
||||
'min_score' => 70,
|
||||
'max_score' => 79,
|
||||
'template_text' => 'Updated comment text',
|
||||
'is_active' => false,
|
||||
]);
|
||||
|
||||
$this->app->instance(AttendanceCommentTemplateService::class, $mock);
|
||||
|
||||
$response = $this->putJson('/api/attendance-comment-templates/7', $payload);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('status', 'success')
|
||||
->assertJsonPath('data.id', 7)
|
||||
->assertJsonPath('data.is_active', false);
|
||||
}
|
||||
|
||||
public function test_update_validation_fails_if_both_scores_invalid_relation(): void
|
||||
{
|
||||
$payload = [
|
||||
'min_score' => 50,
|
||||
'max_score' => 20,
|
||||
];
|
||||
|
||||
$response = $this->putJson('/api/attendance-comment-templates/7', $payload);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['max_score']);
|
||||
}
|
||||
|
||||
public function test_can_delete_template(): void
|
||||
{
|
||||
$mock = Mockery::mock(AttendanceCommentTemplateService::class);
|
||||
$mock->shouldReceive('delete')
|
||||
->once()
|
||||
->with(12);
|
||||
|
||||
$this->app->instance(AttendanceCommentTemplateService::class, $mock);
|
||||
|
||||
$response = $this->deleteJson('/api/attendance-comment-templates/12');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('status', 'success')
|
||||
->assertJsonPath('message', 'Template deleted successfully.');
|
||||
}
|
||||
|
||||
public function test_legacy_list_data_endpoint_returns_templates_key(): void
|
||||
{
|
||||
$mock = Mockery::mock(AttendanceCommentTemplateService::class);
|
||||
$mock->shouldReceive('list')
|
||||
->once()
|
||||
->with(false)
|
||||
->andReturn([
|
||||
[
|
||||
'id' => 1,
|
||||
'min_score' => 0,
|
||||
'max_score' => 49,
|
||||
'template_text' => 'Low attendance',
|
||||
'is_active' => true,
|
||||
]
|
||||
]);
|
||||
|
||||
$this->app->instance(AttendanceCommentTemplateService::class, $mock);
|
||||
|
||||
$response = $this->getJson('/api/attendance-comment-templates/list-data');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonCount(1, 'templates')
|
||||
->assertJsonPath('templates.0.id', 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user