466 lines
13 KiB
PHP
466 lines
13 KiB
PHP
<?php
|
|
|
|
namespace Tests\App\Controllers\Api;
|
|
|
|
use App\Controllers\Api\AttendanceController;
|
|
use CodeIgniter\HTTP\ResponseInterface;
|
|
use CodeIgniter\HTTP\URI;
|
|
use CodeIgniter\HTTP\UserAgent;
|
|
use CodeIgniter\Test\CIUnitTestCase;
|
|
use CodeIgniter\Test\Mock\MockIncomingRequest;
|
|
use Config\App;
|
|
|
|
class FakeAttendanceRequest extends MockIncomingRequest
|
|
{
|
|
public function __construct(private readonly ?array $json = null, private readonly array $gets = [])
|
|
{
|
|
parent::__construct(config(App::class), new URI('https://test.alrahmaisgl.org'), 'php://input', new UserAgent());
|
|
}
|
|
|
|
public function getJSON(bool $assoc = false, int $depth = 512, int $options = 0): ?array
|
|
{
|
|
return $this->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']);
|
|
}
|
|
}
|