add projet

This commit is contained in:
root
2026-03-05 12:29:37 -05:00
parent 8d1eef8ba8
commit 23b7db1107
9109 changed files with 1106501 additions and 73 deletions
@@ -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);
}
}
+236
View File
@@ -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 doesnt 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);
}
}