json; } public function getGet($key = null, $filter = null, $default = null) { if ($key === null) { return $this->gets; } return $this->gets[$key] ?? $default; } } class StubPager { public function __construct(private readonly int $current, private readonly int $perPage, private readonly int $total) { } public function getCurrentPage() { return $this->current; } public function getPerPage() { return $this->perPage; } public function getTotal() { return $this->total; } public function getPageCount() { return (int) ceil($this->total / max(1, $this->perPage)); } } class StubAttendanceModel { public array $whereHistory = []; public array $findResult = []; public array $findAllResult = []; public array $saved = []; public array $attendanceLookup = []; public int $insertId = 0; public array $pagerData = ['current' => 1, 'perPage' => 20, 'total' => 0]; public array $firstQueue = []; public array $updatedAttendance = []; public function where(string $column, $value = null): self { $this->whereHistory[$column] = $value; return $this; } public function paginate(int $perPage, string $group, int $page) { $this->pager = new StubPager($this->pagerData['current'], $this->pagerData['perPage'], $this->pagerData['total']); return $this->findAllResult; } public function find($id) { return $this->findResult[$id] ?? null; } public function insert(array $data) { $this->insertId++; $this->saved[$this->insertId] = $data; $this->findResult[$this->insertId] = ['id' => $this->insertId] + $data; return $this->insertId; } public function getInsertID() { return $this->insertId; } public function update($id, array $data) { $this->saved[$id] = $data; $this->findResult[$id] = ['id' => $id] + $data; } public function delete($id) { unset($this->findResult[$id]); } public function first() { return array_shift($this->firstQueue) ?? null; } public function queueFirst($row): void { $this->firstQueue[] = $row; } public function findAll(): array { return $this->findAllResult; } public function getAttendance(int $studentId, int $classSectionId, string $date): array { return $this->attendanceLookup[$studentId] ?? []; } public function setAttendanceLookup(int $studentId, array $row): void { $this->attendanceLookup[$studentId] = $row; } public function updateAttendance(int $id, array $data): bool { $this->updatedAttendance[$id] = $data; $this->findResult[$id] = ['id' => $id] + $data; return true; } public function orderBy(...$args): self { return $this; } } class StubStudentClassModel { public ?string $grade = null; public function getStudentGrade(int $studentId) { return $this->grade; } } class StubConfigModel { public function __construct(private readonly array $values) { } public function getConfig(string $key) { return $this->values[$key] ?? null; } } class StubDbTable { private array $results = []; public function __construct(array $rows) { $this->results = $rows; } public function select(...$args) { return $this; } public function join(...$args) { return $this; } public function where(...$args) { return $this; } public function orderBy(...$args) { return $this; } public function get() { return $this; } public function getResultArray() { return $this->results; } } class StubDbConnection { public function __construct(private readonly array $rows) { } public function table($name) { return new StubDbTable($this->rows); } } class TestableAttendanceController extends AttendanceController { public bool $validationResult = true; public array $validationErrors = []; public $validator; private ?StubDbConnection $db = null; public function __construct() { } public function setModels(StubAttendanceModel $attendanceModel, StubStudentClassModel $studentClassModel, StubConfigModel $configModel): void { $this->attendanceModel = $attendanceModel; $this->studentClassModel = $studentClassModel; $this->configModel = $configModel; $this->schoolYear = (string) $configModel->getConfig('school_year'); $this->semester = (string) $configModel->getConfig('semester'); } public function setDatabaseConnection(StubDbConnection $db): void { $this->db = $db; } public function setValidationResult(bool $result, array $errors = []): void { $this->validationResult = $result; $this->validationErrors = $errors; } protected function validate($rules, array $messages = []): bool { $this->validator = new class($this->validationErrors) { private array $errors; public function __construct(array $errors) { $this->errors = $errors; } public function getErrors() { return $this->errors; } }; return $this->validationResult; } protected function getDatabaseConnection() { return $this->db ?? parent::getDatabaseConnection(); } protected function respond($data = null, ?int $status = null, string $message = '') { return ['data' => $data, 'code' => $status, 'message' => $message]; } protected function error(string $message = 'An error occurred', int $code = 400, ?array $errors = null) { $payload = ['status' => false, 'message' => $message, 'code' => $code]; if (!empty($errors)) { $payload['errors'] = $errors; } return $payload; } } class AttendanceControllerTest extends CIUnitTestCase { private TestableAttendanceController $controller; private StubAttendanceModel $attendanceModel; private StubStudentClassModel $studentClassModel; private StubConfigModel $configModel; protected function setUp(): void { parent::setUp(); $this->controller = new TestableAttendanceController(); $this->attendanceModel = new StubAttendanceModel(); $this->studentClassModel = new StubStudentClassModel(); $this->configModel = new StubConfigModel(['school_year' => '2024-2025', 'semester' => 'Spring']); $this->controller->setModels($this->attendanceModel, $this->studentClassModel, $this->configModel); } public function testIndexUsesFilters() { $request = new FakeAttendanceRequest(null, ['student_id' => '5', 'date' => '2024-09-01', 'page' => '2', 'per_page' => '10']); $this->attendanceModel->findAllResult = [['id' => 1]]; $this->attendanceModel->pagerData = ['current' => 2, 'perPage' => 10, 'total' => 30]; $this->controller->setRequest($request); $response = $this->controller->index(); $this->assertSame('Attendance records retrieved successfully', $response['message']); $this->assertSame(2, $response['data']['pagination']['current_page']); $this->assertSame(1, $response['data']['data'][0]['id']); } public function testShowNotFound() { $response = $this->controller->show(7); $this->assertSame(ResponseInterface::HTTP_NOT_FOUND, $response['code']); } public function testShowSuccess() { $this->attendanceModel->findResult[8] = ['id' => 8]; $response = $this->controller->show(8); $this->assertSame('Attendance record retrieved successfully', $response['message']); } public function testStoreValidationFails() { $this->controller->setValidationResult(false, ['status' => 'required']); $request = new FakeAttendanceRequest(['student_id'=>1,'date'=>'','status'=>'']); $this->controller->setRequest($request); $response = $this->controller->store(); $this->assertSame(422, $response['code']); $this->assertArrayHasKey('status', $response['errors']); } public function testStoreAlreadyRecorded() { $request = new FakeAttendanceRequest(['student_id'=>2,'date'=>'2024-09-01','status'=>'present']); $this->controller->setRequest($request); $this->attendanceModel->queueFirst(['id'=>5]); $response = $this->controller->store(); $this->assertSame(409, $response['code']); } public function testStoreCreatesRecord() { $request = new FakeAttendanceRequest(['student_id'=>2,'date'=>'2024-09-01','status'=>'present','notes'=>'ok']); $this->controller->setRequest($request); $this->controller->setValidationResult(true); $response = $this->controller->store(); $this->assertSame('Attendance record created successfully', $response['message']); $this->assertSame(1, $this->attendanceModel->insertId); } public function testUpdateNotFound() { $response = $this->controller->update(99); $this->assertSame(ResponseInterface::HTTP_NOT_FOUND, $response['code']); } public function testUpdateValidationFails() { $this->attendanceModel->findResult[12] = ['id' => 12]; $this->controller->setValidationResult(false, ['status' => 'invalid']); $this->controller->setRequest(new FakeAttendanceRequest(['status'=>'bad'])); $response = $this->controller->update(12); $this->assertSame(422, $response['code']); } public function testUpdateSuccess() { $this->attendanceModel->findResult[13] = ['id' => 13]; $this->controller->setValidationResult(true); $this->controller->setRequest(new FakeAttendanceRequest(['status'=>'absent'])); $response = $this->controller->update(13); $this->assertSame('Attendance record updated successfully', $response['message']); $this->assertSame('absent', $this->attendanceModel->findResult[13]['status']); } public function testDeleteNotFound() { $response = $this->controller->delete(20); $this->assertSame(ResponseInterface::HTTP_NOT_FOUND, $response['code']); } public function testDeleteSuccess() { $this->attendanceModel->findResult[21] = ['id' => 21]; $response = $this->controller->delete(21); $this->assertSame('Attendance record deleted successfully', $response['message']); } public function testStudentEndpoint() { $this->attendanceModel->findAllResult = [['id' => 5]]; $response = $this->controller->student(5); $this->assertSame('Student attendance retrieved successfully', $response['message']); } public function testClassRosterAggregatesAttendance() { $stubRows = [ ['student_id' => 1, 'firstname' => 'A', 'lastname' => 'B'], ['student_id' => 2, 'firstname' => 'C', 'lastname' => 'D'], ]; $db = new StubDbConnection($stubRows); $this->controller->setDatabaseConnection($db); $this->controller->setRequest(new FakeAttendanceRequest(null, ['date' => '2024-10-05'])); $this->attendanceModel->setAttendanceLookup(1, ['status' => 'present', 'reason' => '']); $this->attendanceModel->setAttendanceLookup(2, []); $response = $this->controller->classRoster(3); $this->assertSame('Class roster attendance retrieved', $response['message']); $this->assertSame(2, count($response['data']['students'])); } public function testRecordClassValidationFails() { $this->controller->setRequest(new FakeAttendanceRequest(['records' => []])); $response = $this->controller->recordClass(1); $this->assertSame(422, $response['code']); } public function testRecordClassSavesRows() { $this->studentClassModel->grade = '5'; $this->attendanceModel->setAttendanceLookup(7, ['id' => 99]); $payload = [ 'date' => '2024-10-01', 'records' => [ ['student_id' => 7, 'status' => 'present'], ['student_id' => 8, 'status' => 'late'], ], ]; $this->controller->setRequest(new FakeAttendanceRequest($payload)); $response = $this->controller->recordClass(12); $this->assertSame('2 attendance row(s) saved', $response['message']); $this->assertSame(2, $response['data']['saved']); } }