recreate project

This commit is contained in:
root
2026-02-10 22:11:06 -05:00
commit 663c0cdbda
10149 changed files with 1379710 additions and 0 deletions
@@ -0,0 +1,304 @@
<?php
namespace Tests\App\Controllers\Api;
use App\Controllers\Api\AdministratorController;
use CodeIgniter\Test\CIUnitTestCase;
use Config\Services;
class AdministratorControllerTest extends CIUnitTestCase
{
protected $controller;
protected function setUp(): void
{
parent::setUp();
// ✅ Create a fake database connection object
$fakeDb = $this->createMock(\CodeIgniter\Database\BaseConnection::class);
// ✅ Stub Config\Database::connect() globally using class_alias
// so CodeIgniter's static Database::connect() returns our fake DB
if (!class_exists('Config\\FakeDatabase')) {
eval('namespace Config; class FakeDatabase extends \\Config\\Database {
public static function connect($group = null, bool $getShared = true) {
return $GLOBALS["__fakeDb__"];
}
}');
}
$GLOBALS['__fakeDb__'] = $fakeDb;
// Temporarily alias Config\Database to our FakeDatabase
class_alias('Config\\FakeDatabase', 'Config\\Database');
// ✅ Mock all required models/services
foreach ([
\App\Models\UserModel::class,
\App\Models\StudentModel::class,
\App\Models\ConfigurationModel::class,
\App\Models\LoginActivityModel::class,
\App\Models\EnrollmentModel::class,
\App\Models\RefundModel::class,
\App\Models\InvoiceModel::class,
\App\Models\ClassSectionModel::class,
\App\Models\UserRoleModel::class,
\App\Models\StudentClassModel::class,
\App\Models\StaffAttendanceModel::class,
\App\Services\FeeCalculationService::class,
] as $class) {
$this->mockModel($class);
}
// ✅ Configuration mock
$configMock = $this->mockModel(\App\Models\ConfigurationModel::class);
$configMock->method('getConfig')->willReturnMap([
['school_year', '2024-2025'],
['semester', 'Fall'],
]);
// ✅ Instantiate controller safely (no DB hit)
$this->controller = new AdministratorController();
}
protected function mockModel(string $class)
{
$mock = $this->createMock($class);
Services::injectMock($class, $mock);
return $mock;
}
/** @test */
public function testDashboardSuccess()
{
$result = $this->controller->dashboard();
$this->assertNotNull($result);
}
/* --------------------------------------------------------------------------
* absenceInfo()
* -------------------------------------------------------------------------- */
public function testAbsenceInfoWithValidUser()
{
helper('date');
$this->controller->setSession(['user_id' => 7]);
$userMock = model(UserModel::class);
$staffMock = model(StaffAttendanceModel::class);
$userMock->method('find')->willReturn(['firstname' => 'Jane', 'lastname' => 'Admin']);
$staffMock->method('where')->willReturnSelf();
$staffMock->method('orderBy')->willReturnSelf();
$staffMock->method('findAll')->willReturn([
['date' => '2025-09-14', 'status' => 'present'],
]);
$response = $this->controller->absenceInfo();
$this->assertTrue($response['status']);
$this->assertArrayHasKey('available_dates', $response['data']);
}
public function testAbsenceInfoUnauthorized()
{
$this->controller->setSession([]); // no user
$response = $this->controller->absenceInfo();
$this->assertFalse($response['status']);
$this->assertEquals(401, $response['code']);
}
/* --------------------------------------------------------------------------
* submitAbsence()
* -------------------------------------------------------------------------- */
public function testSubmitAbsenceRejectsMissingReason()
{
$this->controller->setSession(['user_id' => 2]);
$req = $this->makeRequest('POST', '/api/v1/administrator/absence', [
'dates' => ['2025-10-05'],
'reason' => '',
]);
$this->controller->setRequest($req);
$resp = $this->controller->submitAbsence();
$this->assertFalse($resp['status']);
$this->assertEquals('Reason is required', $resp['message']);
}
public function testSubmitAbsenceValidDateAndReason()
{
$this->controller->setSession(['user_id' => 5]);
$req = $this->makeRequest('POST', '/api/v1/administrator/absence', [
'dates' => ['2025-09-07'],
'reason_type' => 'Personal',
'reason' => 'Travel',
]);
$this->controller->setRequest($req);
$staffMock = model(StaffAttendanceModel::class);
$userMock = model(UserModel::class);
$userMock->method('getUserRole')->willReturn('admin');
$staffMock->method('upsertOne')->willReturn(true);
$userMock->method('find')->willReturn(['firstname' => 'Sarah', 'lastname' => 'Ali', 'email' => 's@a.com']);
$email = Services::email();
$email->clear();
$resp = $this->controller->submitAbsence();
$this->assertTrue($resp['status']);
$this->assertArrayHasKey('saved_dates', $resp['data']);
}
/* --------------------------------------------------------------------------
* search()
* -------------------------------------------------------------------------- */
public function testSearchRequiresQueryParam()
{
$this->controller->setRequest($this->makeRequest('GET', '/api/v1/administrator/search?q='));
$resp = $this->controller->search();
$this->assertFalse($resp['status']);
$this->assertEquals('Search query is required', $resp['message']);
}
public function testSearchReturnsStructuredResults()
{
$this->controller->setRequest($this->makeRequest('GET', '/api/v1/administrator/search?q=john'));
$db = $this->createMock(\CodeIgniter\Database\BaseConnection::class);
$builder = $this->createMock(\CodeIgniter\Database\BaseBuilder::class);
$builder->method('select')->willReturnSelf();
$builder->method('limit')->willReturnSelf();
$builder->method('get')->willReturnSelf();
$builder->method('getResultArray')->willReturn([
['id' => 1, 'firstname' => 'John', 'lastname' => 'Doe'],
]);
$db->method('table')->willReturn($builder);
service('database')->shouldReceive('connect')->andReturn($db);
$resp = $this->controller->search();
$this->assertTrue($resp['status']);
$this->assertArrayHasKey('users', $resp['data']);
}
/* --------------------------------------------------------------------------
* enrollmentWithdrawal()
* -------------------------------------------------------------------------- */
public function testEnrollmentWithdrawalPagination()
{
$mock = model(EnrollmentModel::class);
$mock->method('where')->willReturnSelf();
$mock->method('orderBy')->willReturnSelf();
$mock->method('findAll')->willReturn([
['student_id' => 1, 'status' => 'enrolled'],
]);
$resp = $this->controller->enrollmentWithdrawal();
$this->assertTrue($resp['status']);
$this->assertEquals('Enrollment/withdrawal data retrieved successfully', $resp['message']);
}
/* --------------------------------------------------------------------------
* enrollmentWithdrawalData()
* -------------------------------------------------------------------------- */
public function testEnrollmentWithdrawalDataBuildsStudentList()
{
$stuMock = model(StudentModel::class);
$stuMock->method('getStudentsWithClassAndEnrollment')->willReturn([
['id' => 1, 'firstname' => 'A', 'lastname' => 'Z', 'parent_id' => 10],
]);
$clsMock = model(ClassSectionModel::class);
$clsMock->method('select')->willReturnSelf();
$clsMock->method('where')->willReturnSelf();
$clsMock->method('orderBy')->willReturnSelf();
$clsMock->method('findAll')->willReturn([
['id' => 1, 'class_section_name' => 'KG1'],
]);
$enrollMock = model(EnrollmentModel::class);
$enrollMock->method('getEnrollmentStatus')->willReturn('enrolled');
$resp = $this->controller->enrollmentWithdrawalData();
$this->assertTrue($resp['status']);
$this->assertArrayHasKey('students', $resp['data']);
$this->assertArrayHasKey('classes', $resp['data']);
}
/* --------------------------------------------------------------------------
* updateEnrollmentStatuses()
* -------------------------------------------------------------------------- */
public function testUpdateEnrollmentStatusesRejectsEmptyPayload()
{
$req = $this->makeRequest('POST', '/api/v1/administrator/enrollment-withdrawal/update', []);
$this->controller->setRequest($req);
$resp = $this->controller->updateEnrollmentStatuses();
$this->assertFalse($resp['status']);
$this->assertEquals('No enrollment statuses were submitted.', $resp['message']);
}
public function testUpdateEnrollmentStatusesWithValidStatus()
{
$req = $this->makeRequest('POST', '/api/v1/administrator/enrollment-withdrawal/update', [
'enrollment_status' => [1 => 'enrolled']
]);
$this->controller->setRequest($req);
$stuMock = model(StudentModel::class);
$stuMock->method('find')->willReturn(['id' => 1, 'parent_id' => 2]);
$enrollMock = model(EnrollmentModel::class);
$enrollMock->method('where')->willReturnSelf();
$enrollMock->method('get')->willReturnSelf();
$enrollMock->method('getRowArray')->willReturn(['enrollment_status' => 'payment pending', 'parent_id' => 2]);
$invoiceMock = model(InvoiceModel::class);
$invoiceMock->method('where')->willReturnSelf();
$invoiceMock->method('orderBy')->willReturnSelf();
$invoiceMock->method('first')->willReturn(['id' => 10, 'school_year' => '2024-2025']);
$refundMock = model(RefundModel::class);
$refundMock->method('where')->willReturnSelf();
$refundMock->method('first')->willReturn(null);
$resp = $this->controller->updateEnrollmentStatuses();
$this->assertTrue($resp['status']);
$this->assertArrayHasKey('groupsByParentStatus', $resp['data']);
}
/* --------------------------------------------------------------------------
* studentProfiles()
* -------------------------------------------------------------------------- */
public function testStudentProfilesReturnsList()
{
$studClassMock = model(StudentClassModel::class);
$studClassMock->method('getClassSectionNameByStudentId')->willReturn('KG2');
$resp = $this->controller->studentProfiles();
$this->assertTrue($resp['status']);
$this->assertArrayHasKey('students', $resp['data']);
}
/* --------------------------------------------------------------------------
* parentProfiles()
* -------------------------------------------------------------------------- */
public function testParentProfilesWithParentRole()
{
$userMock = model(UserModel::class);
$userMock->method('findAll')->willReturn([
['id' => 20, 'firstname' => 'Layla', 'lastname' => 'Yusuf', 'email' => 'layla@test.com', 'cellphone' => '123', 'school_id' => 'P1'],
]);
$roleMock = model(UserRoleModel::class);
$roleMock->method('getRolesByUserId')->willReturn([
['role_name' => 'parent'],
]);
$invMock = model(InvoiceModel::class);
$invMock->method('getLatestInvoicePaidAmount')->willReturn(150.00);
$invMock->method('getLatestInvoiceBalance')->willReturn(50.00);
$resp = $this->controller->parentProfiles();
$this->assertTrue($resp['status']);
$this->assertArrayHasKey('parents', $resp['data']);
$this->assertEquals('Layla', $resp['data']['parents'][0]['firstname']);
}
}
@@ -0,0 +1,282 @@
<?php
namespace Tests\App\Controllers\Api;
use App\Controllers\Api\AssignmentController;
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 FakeJsonRequest extends MockIncomingRequest
{
public function __construct(private readonly ?array $payload)
{
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->payload;
}
}
class FakeGetRequest extends MockIncomingRequest
{
public function __construct(private readonly array $gets = [])
{
parent::__construct(config(App::class), new URI('https://test.alrahmaisgl.org'), 'php://input', new UserAgent());
}
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 StubAssignmentModel
{
public array $whereHistory = [];
public array $saved = [];
public int $nextId = 10;
public array $findAllResult = [];
public array $findResult = [];
public array $pagerData = ['current' => 1, 'perPage' => 20, 'total' => 0];
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->nextId++;
$this->saved[$this->nextId] = $data;
$this->findResult[$this->nextId] = ['id' => $this->nextId] + $data;
return $this->nextId;
}
public function getInsertID()
{
return $this->nextId;
}
public function update($id, array $data)
{
$this->saved[$id] = $data;
$this->findResult[$id] = ['id' => $id] + $data;
return true;
}
public function delete($id)
{
unset($this->findResult[$id]);
return true;
}
public function findAll()
{
return $this->findAllResult;
}
}
class TestableAssignmentController extends AssignmentController
{
public bool $validationResult = true;
public array $validationErrors = [];
public $validator;
public function __construct()
{
// skip real initialization
}
public function setAssignmentModel(StubAssignmentModel $model): void
{
$this->assignmentModel = $model;
}
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 respond($data = null, ?int $status = null, string $message = '')
{
return ['data' => $data, 'code' => $status, 'message' => $message];
}
}
class AssignmentControllerTest extends CIUnitTestCase
{
private TestableAssignmentController $controller;
private StubAssignmentModel $model;
protected function setUp(): void
{
parent::setUp();
$this->controller = new TestableAssignmentController();
$this->model = new StubAssignmentModel();
$this->controller->setAssignmentModel($this->model);
}
public function testIndexAppliesFilters()
{
$request = new FakeGetRequest(['page' => '2', 'per_page' => '5', 'class_id' => '12', 'teacher_id' => '3', 'student_id' => '7']);
$this->controller->setRequest($request);
$this->model->findAllResult = [['id' => 1]];
$this->model->pagerData = ['current' => 2, 'perPage' => 5, 'total' => 11];
$response = $this->controller->index();
$this->assertSame('Assignments retrieved successfully', $response['message']);
$this->assertSame(['id' => 1], $response['data']['data'][0]);
$this->assertSame(2, $response['data']['pagination']['current_page']);
$this->assertCount(3, $this->model->whereHistory);
}
public function testShowNotFound()
{
$response = $this->controller->show(99);
$this->assertSame(ResponseInterface::HTTP_NOT_FOUND, $response['code']);
}
public function testShowSuccess()
{
$this->model->findResult[5] = ['id' => 5];
$response = $this->controller->show(5);
$this->assertSame('Assignment retrieved successfully', $response['message']);
$this->assertSame(['id' => 5], $response['data']);
}
public function testStoreValidationFails()
{
$this->controller->setValidationResult(false, ['title' => 'required']);
$request = new FakeJsonRequest(['title' => '']);
$this->controller->setRequest($request);
$response = $this->controller->store();
$this->assertSame(422, $response['code']);
$this->assertArrayHasKey('title', $response['errors']);
}
public function testStoreCreatesAssignment()
{
$request = new FakeJsonRequest([
'title' => 'Test',
'description' => '',
'due_date' => '2025-01-01',
'class_id' => 1,
'teacher_id' => 2,
]);
$this->controller->setRequest($request);
$this->controller->setValidationResult(true);
$response = $this->controller->store();
$this->assertSame('Assignment created successfully', $response['message']);
$this->assertSame('Test', $this->model->saved[$this->model->getInsertID()]['title']);
}
public function testUpdateNotFound()
{
$response = $this->controller->update(11);
$this->assertSame(ResponseInterface::HTTP_NOT_FOUND, $response['code']);
}
public function testUpdateModifiesAssignment()
{
$this->model->findResult[15] = ['id' => 15, 'title' => 'Old'];
$request = new FakeJsonRequest(['title' => 'New']);
$this->controller->setRequest($request);
$this->controller->setValidationResult(true);
$response = $this->controller->update(15);
$this->assertSame('Assignment updated successfully', $response['message']);
$this->assertSame('New', $this->model->findResult[15]['title']);
}
public function testDeleteFlow()
{
$this->model->findResult[20] = ['id' => 20];
$response = $this->controller->delete(20);
$this->assertSame('Assignment deleted successfully', $response['message']);
$this->assertArrayNotHasKey(20, $this->model->findResult);
}
public function testStudentReturnsList()
{
$this->model->findAllResult = [['id' => 1]];
$response = $this->controller->student(4);
$this->assertSame('Assignments retrieved successfully for student', $response['message']);
$this->assertNotEmpty($response['data']);
}
}
@@ -0,0 +1,465 @@
<?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']);
}
}
@@ -0,0 +1,229 @@
<?php
namespace Tests\App\Controllers\Api;
use App\Controllers\Api\AttendanceTrackingController;
use CodeIgniter\HTTP\URI;
use CodeIgniter\HTTP\UserAgent;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\Mock\MockIncomingRequest;
use Config\App;
class FakeTrackingRequest 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 StubTrackingModel
{
public array $findAllResult = [];
public array $findResult = [];
public array $updated = [];
public array $inserted = [];
public array $firstQueue = [];
public function where($column, $value = null)
{
return $this;
}
public function select($columns)
{
return $this;
}
public function groupBy($column)
{
return $this;
}
public function having($column, $value)
{
return $this;
}
public function findAll(): array
{
return $this->findAllResult;
}
public function orderBy(...$args)
{
return $this;
}
public function first()
{
return array_shift($this->firstQueue) ?? null;
}
public function queueFirst(array $row): void
{
$this->firstQueue[] = $row;
}
public function insert(array $data)
{
$this->inserted[] = $data;
return 1;
}
public function getInsertID()
{
return 1;
}
public function update(int $id, array $data)
{
$this->updated[$id] = $data;
}
public function find(int $id)
{
return $this->findResult[$id] ?? null;
}
}
class TestableAttendanceTrackingController extends AttendanceTrackingController
{
public bool $validationResult = true;
public array $validationErrors = [];
public $validator;
public function __construct()
{
// bypass real model instantiation
}
public function setTrackingModel(StubTrackingModel $model): void
{
$this->trackingModel = $model;
}
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 respond($data = null, ?int $status = null, string $message = '')
{
return ['data' => $data, 'code' => $status, 'message' => $message];
}
}
class AttendanceTrackingControllerTest extends CIUnitTestCase
{
private TestableAttendanceTrackingController $controller;
private StubTrackingModel $model;
protected function setUp(): void
{
parent::setUp();
$this->controller = new TestableAttendanceTrackingController();
$this->model = new StubTrackingModel();
$this->controller->setTrackingModel($this->model);
}
public function testRecordValidationFails()
{
$this->controller->setValidationResult(false, ['student_id' => 'required']);
$this->controller->setRequest(new FakeTrackingRequest(['student_id' => null]));
$response = $this->controller->record();
$this->assertSame(422, $response['code']);
$this->assertArrayHasKey('student_id', $response['errors']);
}
public function testRecordUpdatesExisting()
{
$this->model->queueFirst(['id' => 5]);
$request = new FakeTrackingRequest(['student_id' => 1, 'date' => '2024-09-01', 'status' => 'present']);
$this->controller->setRequest($request);
$response = $this->controller->record();
$this->assertSame('Attendance tracking record updated successfully', $response['message']);
$this->assertSame(['student_id' => 1, 'date' => '2024-09-01', 'status' => 'present'], $this->model->updated[5]);
}
public function testRecordCreatesNew()
{
$request = new FakeTrackingRequest(['student_id' => 2, 'date' => '2024-09-02', 'status' => 'late']);
$this->controller->setRequest($request);
$this->controller->setValidationResult(true);
$response = $this->controller->record();
$this->assertSame('Attendance tracking record created successfully', $response['message']);
$this->assertSame(1, $response['data']['id']);
}
public function testViolationsDefaultsThreshold()
{
$this->controller->setRequest(new FakeTrackingRequest(null, []));
$this->model->findAllResult = [['student_id' => 3, 'total_violations' => 5]];
$response = $this->controller->violations();
$this->assertSame('Attendance violations retrieved successfully', $response['message']);
$this->assertSame(1, count($response['data']));
}
public function testViolationsCustomThreshold()
{
$request = new FakeTrackingRequest(null, ['threshold' => '2']);
$this->controller->setRequest($request);
$this->model->findAllResult = [['student_id' => 4, 'total_violations' => 3]];
$response = $this->controller->violations();
$this->assertSame(1, count($response['data']));
}
public function testStudentRecordsOrder()
{
$this->model->findAllResult = [['id' => 7]];
$response = $this->controller->student(7);
$this->assertSame('Tracking records retrieved successfully for student', $response['message']);
$this->assertSame(7, $response['data'][0]['id']);
}
}
@@ -0,0 +1,180 @@
<?php
namespace Tests\App\Controllers\Api;
use App\Controllers\Api\BroadcastEmailController;
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 FakeBroadcastRequest extends MockIncomingRequest
{
public function __construct(private readonly ?array $json = null)
{
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;
}
}
class StubBroadcastModel
{
public array $inserted = [];
public function insert(array $data)
{
$this->inserted[] = $data;
return 1;
}
}
class StubUserModel
{
public function __construct(private readonly array $parents = [])
{
}
public function select($columns)
{
return $this;
}
public function join(...$args)
{
return $this;
}
public function where(...$args)
{
return $this;
}
public function findAll(): array
{
return $this->parents;
}
}
class TestableBroadcastEmailController extends BroadcastEmailController
{
public bool $validationResult = true;
public array $validationErrors = [];
public $validator;
public function __construct()
{
// keep the parent constructor from running to avoid real models
}
public function setBroadcastModel(StubBroadcastModel $model): void
{
$this->broadcastModel = $model;
}
public function setUserModel(StubUserModel $model): void
{
$this->userModel = $model;
}
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) {
public function __construct(private readonly array $errors)
{
}
public function getErrors(): array
{
return $this->errors;
}
};
return $this->validationResult;
}
protected function respondSuccess($data = null, string $message = 'Success', int $code = ResponseInterface::HTTP_OK)
{
return ['status' => true, 'message' => $message, 'data' => $data, 'code' => $code];
}
protected function respondValidationError(array $errors, string $message = 'Validation failed')
{
return ['status' => false, 'message' => $message, 'errors' => $errors, 'code' => ResponseInterface::HTTP_UNPROCESSABLE_ENTITY];
}
}
class BroadcastEmailControllerTest extends CIUnitTestCase
{
private TestableBroadcastEmailController $controller;
private StubBroadcastModel $broadcastModel;
protected function setUp(): void
{
parent::setUp();
$this->controller = new TestableBroadcastEmailController();
$this->broadcastModel = new StubBroadcastModel();
$this->controller->setBroadcastModel($this->broadcastModel);
}
public function testSendValidationFails(): void
{
$this->controller->setValidationResult(false, ['subject' => 'required']);
$this->controller->setRequest(new FakeBroadcastRequest(['subject' => '', 'body' => '', 'recipients' => []]));
$response = $this->controller->send();
$this->assertSame(ResponseInterface::HTTP_UNPROCESSABLE_ENTITY, $response['code']);
$this->assertSame('Validation failed', $response['message']);
$this->assertArrayHasKey('subject', $response['errors']);
}
public function testSendStoresSentAndFailedRecipients(): void
{
$requestData = [
'subject' => 'Weekly Update',
'body' => 'Hello parents',
'recipients' => ['parent1@example.com', 'bad-email'],
];
$this->controller->setRequest(new FakeBroadcastRequest($requestData));
$response = $this->controller->send();
$this->assertSame(ResponseInterface::HTTP_OK, $response['code']);
$this->assertSame('Broadcast email sent successfully', $response['message']);
$this->assertSame(['parent1@example.com'], $response['data']['sent']);
$this->assertSame(['bad-email'], $response['data']['failed']);
$this->assertCount(1, $this->broadcastModel->inserted);
$data = $this->broadcastModel->inserted[0];
$this->assertSame('Weekly Update', $data['subject']);
$this->assertSame('Hello parents', $data['body']);
$this->assertSame(1, $data['total_sent']);
$this->assertArrayHasKey('created_at', $data);
}
public function testParentsReturnsParentList(): void
{
$parents = [
['id' => 10, 'firstname' => 'Test', 'lastname' => 'Parent', 'email' => 'test@parent.com'],
];
$this->controller->setUserModel(new StubUserModel($parents));
$response = $this->controller->parents();
$this->assertSame(ResponseInterface::HTTP_OK, $response['code']);
$this->assertSame('Parent list retrieved successfully', $response['message']);
$this->assertSame($parents, $response['data']);
}
}
@@ -0,0 +1,142 @@
<?php
namespace Tests\App\Controllers\Api;
use App\Controllers\Api\ContactController;
use App\Controllers\View\EmailController;
use CodeIgniter\Test\CIUnitTestCase;
use Config\Services;
class FakeJsonRequest
{
public function __construct(private readonly ?array $payload)
{
}
public function getJSON(bool $assoc = false): ?array
{
return $this->payload;
}
}
class StubEmailController extends EmailController
{
public function __construct(private readonly bool $result)
{
}
public function sendEmail(
string $recipient,
string $subject,
string $htmlMessage,
?string $profile = null,
?string $replyToEmail = null,
?string $replyToName = null,
array $attachments = []
): bool {
return $this->result;
}
}
class TestableContactController extends ContactController
{
private ?EmailController $emailController = null;
public function __construct()
{
// Skip the parent bootstrap so we control dependencies manually.
}
public function setEmailController(EmailController $emailController): void
{
$this->emailController = $emailController;
}
protected function createEmailController(): EmailController
{
return $this->emailController ?? parent::createEmailController();
}
}
class ContactControllerTest extends CIUnitTestCase
{
private TestableContactController $controller;
protected function setUp(): void
{
parent::setUp();
Services::resetSingle('validation');
$this->controller = new TestableContactController();
}
public function testSubmitReturnsErrorWhenJsonMissing()
{
$this->setRequestPayload(null);
$response = $this->controller->submit();
$this->assertFalse($response['status']);
$this->assertSame(400, $response['code']);
$this->assertSame('Invalid request data', $response['message']);
}
public function testSubmitReturnsValidationErrors()
{
$this->setRequestPayload([
'name' => 'Al',
'email' => 'invalid',
'subject' => 'Hi',
'message' => 'short',
]);
$response = $this->controller->submit();
$this->assertFalse($response['status']);
$this->assertSame(422, $response['code']);
$this->assertSame('Validation failed', $response['message']);
$this->assertArrayHasKey('errors', $response);
$this->assertNotEmpty($response['errors']);
}
public function testSubmitReturnsSuccessWhenEmailSent()
{
$this->controller->setEmailController(new StubEmailController(true));
$this->setRequestPayload($this->validPayload());
$response = $this->controller->submit();
$this->assertTrue($response['status']);
$this->assertSame('Thank you for contacting us! We will get back to you soon.', $response['message']);
$this->assertNull($response['data']);
}
public function testSubmitReturnsErrorWhenEmailFails()
{
$this->controller->setEmailController(new StubEmailController(false));
$this->setRequestPayload($this->validPayload());
$response = $this->controller->submit();
$this->assertFalse($response['status']);
$this->assertSame(500, $response['code']);
$this->assertSame('There was an error sending your message. Please try again later.', $response['message']);
}
private function setRequestPayload(?array $payload): void
{
$request = new FakeJsonRequest($payload);
self::setPrivateProperty($this->controller, 'request', $request);
}
private function validPayload(): array
{
return [
'name' => 'Alia Ahmed',
'email' => 'alia@alrahmaisgl.org',
'subject' => 'Support request',
'message' => str_repeat('Please assist. ', 5),
];
}
}
@@ -0,0 +1,30 @@
<?php
namespace Tests\App\Controllers\Api;
use App\Controllers\Api\DocsController;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Test\CIUnitTestCase;
class TestableDocsController extends DocsController
{
protected function success($data = null, string $message = 'Success', int $code = ResponseInterface::HTTP_OK)
{
return ['data' => $data, 'message' => $message, 'code' => $code];
}
}
class DocsControllerTest extends CIUnitTestCase
{
public function testSwaggerReturnsTwoSpecs()
{
$controller = new TestableDocsController();
$response = $controller->swagger();
$this->assertSame(ResponseInterface::HTTP_OK, $response['code']);
$this->assertSame('Available OpenAPI specifications', $response['message']);
$this->assertIsArray($response['data']);
$this->assertCount(2, $response['data']);
$this->assertSame('/docs/openapi_administrator_controller.yaml', $response['data'][0]['url']);
}
}
@@ -0,0 +1,104 @@
<?php
namespace Tests\App\Controllers\Api;
use App\Controllers\Api\EmergencyContactController;
use App\Models\ConfigurationModel;
use App\Models\EmergencyContactModel;
use CodeIgniter\Test\CIUnitTestCase;
class TestableEmergencyContactController extends EmergencyContactController
{
public function setModels(EmergencyContactModel $model, ConfigurationModel $config): void
{
$this->emergencyContactModel = $model;
$this->configModel = $config;
}
}
class EmergencyContactControllerTest extends CIUnitTestCase
{
private $controller;
private $contactModel;
private $configModel;
protected function setUp(): void
{
parent::setUp();
$this->contactModel = $this->createMock(EmergencyContactModel::class);
$this->configModel = $this->createMock(ConfigurationModel::class);
$this->controller = new TestableEmergencyContactController();
$this->controller->setModels($this->contactModel, $this->configModel);
}
public function testIndexFiltersByParent()
{
$query = $this->makeRequest('GET', '/api/v1/emergency-contacts', ['parent_id' => 9]);
$this->controller->setRequest($query);
$this->configModel->method('getConfig')->willReturnMap([
['school_year', '2024-2025'],
['semester', 'Fall'],
]);
$this->contactModel->method('where')->willReturnSelf();
$this->contactModel->method('findAll')->willReturn([
['id' => 1, 'parent_id' => 9, 'cellphone' => '5551234'],
]);
$response = $this->controller->index();
$this->assertTrue($response['status']);
$this->assertCount(1, $response['data']);
}
public function testShowReturnsNotFound()
{
$this->contactModel->method('find')->with(5)->willReturn(null);
$response = $this->controller->show(5);
$this->assertFalse($response['status']);
$this->assertEquals(404, $response['code']);
}
public function testCreateValidContact()
{
$payload = [
'parent_id' => 5,
'emergency_contact_name' => 'Hakim',
'cellphone' => '555-1234',
'relation' => 'Uncle',
'school_year' => '2024-2025',
'semester' => 'Fall',
];
$req = $this->makeRequest('POST', '/api/v1/emergency-contacts', $payload);
$this->controller->setRequest($req);
$this->configModel->method('getConfig')->willReturnMap([
['school_year', '2024-2025'],
['semester', 'Fall'],
]);
$this->contactModel->method('insert')->willReturn(99);
$this->contactModel->method('find')->with(99)->willReturn([
'id' => 99,
'emergency_contact_name' => 'Hakim',
]);
$response = $this->controller->create();
$this->assertTrue($response['status']);
$this->assertEquals('Emergency contact created successfully', $response['message']);
$this->assertEquals(99, $response['data']['id']);
}
public function testCreateInvalidPayload()
{
$req = $this->makeRequest('POST', '/api/v1/emergency-contacts', []);
$this->controller->setRequest($req);
$response = $this->controller->create();
$this->assertFalse($response['status']);
$this->assertEquals(422, $response['code']);
}
}
@@ -0,0 +1,129 @@
<?php
namespace Tests\App\Controllers\Api;
use App\Controllers\Api\FamilyController;
use App\Models\FamilyGuardianModel;
use App\Models\FamilyModel;
use App\Models\FamilyStudentModel;
use CodeIgniter\Test\CIUnitTestCase;
class TestableFamilyController extends FamilyController
{
private array $overridePagination = [];
public function setModels(FamilyModel $familyModel, FamilyStudentModel $familyStudentModel, FamilyGuardianModel $familyGuardianModel): void
{
$this->familyModel = $familyModel;
$this->familyStudentModel = $familyStudentModel;
$this->familyGuardianModel = $familyGuardianModel;
}
public function setPaginatedResult(array $result): void
{
$this->overridePagination = $result;
}
protected function paginate($source, int $page = 1, int $perPage = 20): array
{
if (!empty($this->overridePagination)) {
return $this->overridePagination;
}
return parent::paginate($source, $page, $perPage);
}
}
class FamilyControllerTest extends CIUnitTestCase
{
private $controller;
private $familyModel;
private $studentModel;
private $guardianModel;
protected function setUp(): void
{
parent::setUp();
$this->familyModel = $this->createMock(FamilyModel::class);
$this->studentModel = $this->createMock(FamilyStudentModel::class);
$this->guardianModel = $this->createMock(FamilyGuardianModel::class);
$this->controller = new TestableFamilyController();
$this->controller->setModels($this->familyModel, $this->studentModel, $this->guardianModel);
}
public function testGetGuardiansReturnsDataWhenFamilyExists()
{
$this->familyModel->method('find')->with(5)->willReturn(['id' => 5]);
$this->guardianModel->method('where')->willReturnSelf();
$this->guardianModel->method('findAll')->willReturn([
['id' => 11, 'cellphone' => '555-0101'],
]);
$response = $this->controller->getGuardians(5);
$this->assertTrue($response['status']);
$this->assertCount(1, $response['data']);
}
public function testGetGuardiansReturnsErrorWhenFamilyMissing()
{
$this->familyModel->method('find')->with(3)->willReturn(null);
$response = $this->controller->getGuardians(3);
$this->assertFalse($response['status']);
$this->assertEquals(404, $response['code']);
}
public function testGetByStudentWrapsStudentsAndGuardians()
{
$this->studentModel->method('where')->willReturnSelf();
$this->studentModel->method('findColumn')->willReturn([10]);
$this->familyModel->method('whereIn')->willReturnSelf();
$this->familyModel->method('findAll')->willReturn([
['id' => 10, 'family_name' => 'Main Family'],
]);
$this->studentModel->method('findAll')->willReturn([
['id' => 222, 'firstname' => 'Amina'],
]);
$this->guardianModel->method('where')->willReturnSelf();
$this->guardianModel->method('findAll')->willReturn([
['id' => 33, 'firstname' => 'Derrell'],
]);
$response = $this->controller->getByStudent(42);
$this->assertTrue($response['status']);
$this->assertEquals('Families retrieved successfully', $response['message']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('students', $response['data'][0]);
}
public function testIndexUsesPaginatedResult()
{
$paginatedPayload = [
'data' => [
['id' => 1, 'family_name' => 'Alver Family'],
],
'pagination' => ['current_page' => 1, 'per_page' => 20, 'total' => 1, 'total_pages' => 1],
];
$this->controller->setPaginatedResult($paginatedPayload);
$response = $this->controller->index();
$this->assertTrue($response['status']);
$this->assertSame($paginatedPayload, $response['data']);
}
public function testShowReturnsFamily()
{
$this->familyModel->method('find')->with(17)->willReturn(['id' => 17, 'family_name' => 'Mansour']);
$this->studentModel->method('where')->willReturnSelf();
$this->studentModel->method('findAll')->willReturn([]);
$this->guardianModel->method('where')->willReturnSelf();
$this->guardianModel->method('findAll')->willReturn([]);
$response = $this->controller->show(17);
$this->assertTrue($response['status']);
$this->assertEquals('Family retrieved successfully', $response['message']);
}
}
@@ -0,0 +1,141 @@
<?php
namespace Tests\App\Controllers\Api;
use App\Controllers\Api\FilesController;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\URI;
use CodeIgniter\HTTP\UserAgent;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\Mock\MockIncomingRequest;
use Config\App;
use Config\Services;
class FakeFilesRequest extends MockIncomingRequest
{
public function __construct(private readonly array $gets = [])
{
parent::__construct(config(App::class), new URI('https://test.alrahmaisgl.org'), 'php://input', new UserAgent());
}
public function getGet($key = null, $filter = null, $default = null)
{
if ($key === null) {
return $this->gets;
}
return $this->gets[$key] ?? $default;
}
}
class FakeStreamResponse
{
public array $headers = [];
public string $body = '';
public function setHeader(string $key, string $value)
{
$this->headers[$key] = $value;
return $this;
}
public function setBody(string $body)
{
$this->body = $body;
return $this;
}
}
class TestableFilesController extends FilesController
{
protected function respond($data = null, ?int $status = null, string $message = '')
{
$payload = $data;
if (is_array($data) && isset($data['message'])) {
$message = $data['message'] ?? $message;
$payload = $data['data'] ?? $data;
}
return [
'status' => true,
'message' => $message,
'data' => $payload,
'code' => $status,
];
}
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 FilesControllerTest extends CIUnitTestCase
{
private TestableFilesController $controller;
protected function setUp(): void
{
parent::setUp();
Services::resetSingle('session');
$this->controller = new TestableFilesController();
$response = new FakeStreamResponse();
self::setPrivateProperty($this->controller, 'response', $response);
}
public function testReceiptRejectsInvalidFilename()
{
$response = $this->controller->receipt('../secret.txt');
$this->assertFalse($response['status']);
$this->assertSame(400, $response['code']);
}
public function testReceiptRejectsUnsupportedExtension()
{
$response = $this->controller->receipt('malware.exe');
$this->assertFalse($response['status']);
$this->assertSame(400, $response['code']);
}
public function testReceiptReturnsNotFoundWhenMissing()
{
$response = $this->controller->receipt('missing.pdf');
$this->assertFalse($response['status']);
$this->assertSame(404, $response['code']);
}
public function testIndexRejectsInvalidType()
{
$request = new FakeFilesRequest(['type' => 'avatars']);
$this->controller->setRequest($request);
$response = $this->controller->index();
$this->assertFalse($response['status']);
$this->assertSame(400, $response['code']);
}
public function testIndexReturnsFileList()
{
$request = new FakeFilesRequest(['type' => 'receipts']);
$this->controller->setRequest($request);
$response = $this->controller->index();
$this->assertTrue($response['status']);
$this->assertSame('Files retrieved successfully', $response['message']);
$this->assertSame('receipts', $response['data']['type']);
$this->assertGreaterThanOrEqual(0, $response['data']['count']);
$names = array_column($response['data']['files'], 'name');
$this->assertIsArray($names);
}
}
@@ -0,0 +1,104 @@
<?php
namespace Tests\App\Controllers\Api;
use App\Controllers\Api\HealthController;
use CodeIgniter\Test\CIUnitTestCase;
use Config\Services;
use CodeIgniter\HTTP\IncomingRequest;
class StubHealthController extends HealthController
{
private $dbConnection;
public function setDatabaseConnection($connection): void
{
$this->dbConnection = $connection;
}
protected function getDatabaseConnection()
{
return $this->dbConnection ?? parent::getDatabaseConnection();
}
protected function respond($data = null, ?int $status = null, string $message = '')
{
$payload = is_array($data) ? $data : [];
$payload['code'] = $status;
return $payload;
}
}
class StubDbConnection
{
public function __construct(
private array $tables = [],
private array $fields = []
) {
}
public function tableExists(string $table): bool
{
return $this->tables[$table] ?? true;
}
public function fieldExists(string $field, string $table): bool
{
return $this->fields[$table][$field] ?? true;
}
}
class HealthControllerTest extends CIUnitTestCase
{
private StubHealthController $controller;
protected function setUp(): void
{
parent::setUp();
$this->controller = new StubHealthController();
$request = $this->createMock(IncomingRequest::class);
$this->controller->setRequest($request);
$response = Services::response();
self::setPrivateProperty($this->controller, 'response', $response);
}
public function testIndexReportsOkWhenAllChecksPass()
{
$stubDb = new StubDbConnection(
['user_preferences' => true, 'settings' => true, 'migrations' => true],
[
'user_preferences' => ['timezone' => true],
'settings' => ['timezone' => true],
]
);
$this->controller->setDatabaseConnection($stubDb);
$result = $this->controller->index();
$this->assertArrayHasKey('ok', $result);
$this->assertTrue($result['ok']);
$this->assertSame(WRITEPATH, $result['write_path']);
$this->assertSame(200, $result['code']);
$this->assertSame(true, $result['database']['user_preferences_exists']);
}
public function testIndexReturns503WhenFieldMissing()
{
$stubDb = new StubDbConnection(
['user_preferences' => true, 'settings' => true, 'migrations' => true],
[
'user_preferences' => ['timezone' => false],
'settings' => ['timezone' => true],
]
);
$this->controller->setDatabaseConnection($stubDb);
$result = $this->controller->index();
$this->assertArrayHasKey('ok', $result);
$this->assertFalse($result['ok']);
$this->assertSame(503, $result['code']);
$this->assertFalse($result['database']['user_preferences_has_timezone']);
}
}
@@ -0,0 +1,109 @@
<?php
namespace Tests\App\Controllers\Api;
use App\Controllers\Api\InfoIconController;
use App\Models\UserModel;
use CodeIgniter\Test\CIUnitTestCase;
class StubInfoIconUserModel extends UserModel
{
public function __construct()
{
// skip parent initialization to avoid DB dependencies
}
private array $data = [];
public function setUserInfoById(int $id, ?array $info): void
{
$this->data[$id] = $info;
}
public function getUserInfoById($id): ?array
{
return $this->data[$id] ?? null;
}
}
class TestableInfoIconController extends InfoIconController
{
private ?object $user = null;
private ?StubInfoIconUserModel $userModel = null;
public function setCurrentUser(?object $user): void
{
$this->user = $user;
}
public function setUserModel(StubInfoIconUserModel $model): void
{
$this->userModel = $model;
}
protected function getCurrentUser(): ?object
{
return $this->user;
}
protected function createUserModel(): UserModel
{
return $this->userModel ?? parent::createUserModel();
}
protected function respond($data = null, ?int $status = null, string $message = '')
{
return ['status' => true, 'code' => $status, 'message' => $message, 'data' => $data];
}
protected function error(string $message = 'An error occurred', int $code = 400, ?array $errors = null)
{
return ['status' => false, 'code' => $code, 'message' => $message, 'errors' => $errors];
}
}
class InfoIconControllerTest extends CIUnitTestCase
{
private TestableInfoIconController $controller;
private StubInfoIconUserModel $userModel;
protected function setUp(): void
{
parent::setUp();
$this->controller = new TestableInfoIconController();
$this->userModel = new StubInfoIconUserModel();
$this->controller->setUserModel($this->userModel);
}
public function testRequiresAuthentication()
{
$response = $this->controller->profileIcon();
$this->assertFalse($response['status']);
$this->assertSame(401, $response['code']);
}
public function testReturnsNotFoundWhenUserMissing()
{
$this->controller->setCurrentUser((object) ['id' => 12]);
$response = $this->controller->profileIcon();
$this->assertFalse($response['status']);
$this->assertSame(404, $response['code']);
}
public function testReturnsUserProfileInfo()
{
$this->controller->setCurrentUser((object) ['id' => 7]);
$this->userModel->setUserInfoById(7, ['firstname' => 'Lana', 'lastname' => 'Kay', 'email' => 'test']);
$response = $this->controller->profileIcon();
$this->assertTrue($response['status']);
$this->assertSame('Profile icon info retrieved successfully', $response['message']);
$this->assertSame('Lana Kay', $response['data']['user_name']);
$this->assertSame('LK', $response['data']['user_initials']);
}
}
@@ -0,0 +1,257 @@
<?php
namespace Tests\App\Controllers\Api;
use App\Controllers\Api\MessagesController;
use App\Models\MessageModel as BaseMessageModel;
use App\Models\StudentClassModel as BaseStudentClassModel;
use App\Models\StudentModel as BaseStudentModel;
use App\Models\TeacherClassModel as BaseTeacherClassModel;
use App\Models\UserModel as BaseUserModel;
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 FakeMessagesRequest extends MockIncomingRequest
{
public function __construct(private readonly ?array $json = null)
{
parent::__construct(config(App::class), new URI('http://example.com'), 'php://input', new UserAgent());
}
public function getJSON(bool $assoc = false, int $depth = 512, int $options = 0): ?array
{
return $this->json;
}
public function getPost($key = null, $filter = null, $default = null)
{
if ($key === null) {
return $this->json ?? [];
}
return $this->json[$key] ?? $default;
}
}
class StubMessageModel extends BaseMessageModel
{
public array $inbox = [];
public array $sent = [];
public array $drafts = [];
public array $trashed = [];
public array $inserted = [];
public array $marked = [];
public function __construct()
{
// avoid parent initialization
}
public function getInboxMessages($userId)
{
return $this->inbox;
}
public function getSentMessages($userId)
{
return $this->sent;
}
public function getDraftMessages($userId)
{
return $this->drafts;
}
public function getTrashedMessages($userId)
{
return $this->trashed;
}
public function insert($row = null, bool $returnID = true)
{
$this->inserted = is_array($row) ? $row : [];
return 123;
}
public function find($id = null)
{
return ['id' => $id ?? 0] + $this->inserted;
}
public function markAsRead($messageId)
{
$this->marked[] = $messageId;
}
}
class StubTeacherClassModel extends BaseTeacherClassModel
{
public array $data = [];
public function findAll($limit = 0, $offset = 0)
{
return $this->data;
}
}
class StubStudentClassModel extends BaseStudentClassModel
{
public array $perClass = [];
private $lastClass = null;
public function where($column, $value)
{
if ($column === 'class_section_id') {
$this->lastClass = $value;
}
return $this;
}
public function findAll($limit = 0, $offset = 0)
{
return $this->perClass[$this->lastClass] ?? [];
}
}
class StubStudentModel extends BaseStudentModel
{
public array $students = [];
public function find($id = null)
{
return $this->students[$id] ?? null;
}
}
class StubUserModel extends BaseUserModel
{
public array $users = [];
public function find($id = null)
{
return $this->users[$id] ?? null;
}
}
class TestableMessagesController extends MessagesController
{
public StubMessageModel $stubMessageModel;
public StubTeacherClassModel $stubTeacherClassModel;
public StubStudentClassModel $stubStudentClassModel;
public StubStudentModel $stubStudentModel;
public StubUserModel $stubUserModel;
public array $receivedMessages = [];
public array $read = [];
public function __construct()
{
// intentionally skip parent constructor
}
protected function success($data = null, string $message = 'Success', int $code = ResponseInterface::HTTP_OK)
{
return ['data' => $data, 'message' => $message, 'code' => $code];
}
protected function error(string $message = 'An error occurred', int $code = ResponseInterface::HTTP_BAD_REQUEST, ?array $errors = null)
{
return ['message' => $message, 'code' => $code, 'errors' => $errors];
}
protected function getCurrentUser(): ?object
{
return (object)[
'id' => 42,
'roles' => ['administrator'],
];
}
protected function getReceivedMessages(int $userId): array
{
return $this->receivedMessages;
}
protected function markAsRead(int $messageId): void
{
$this->read[] = $messageId;
}
protected function generateMessageNumber(): string
{
return 'MSG-TEST';
}
protected function getRecipientId(array $role): ?int
{
return 99;
}
public function setModels()
{
$this->messageModel = new StubMessageModel();
$this->teacherClassModel = new StubTeacherClassModel();
$this->studentClassModel = new StubStudentClassModel();
$this->studentModel = new StubStudentModel();
$this->userModel = new StubUserModel();
}
}
class MessagesControllerTest extends CIUnitTestCase
{
private TestableMessagesController $controller;
protected function setUp(): void
{
parent::setUp();
$this->controller = new TestableMessagesController();
$this->controller->setModels();
}
public function testInboxUsesMessageModel()
{
$this->controller->messageModel->inbox = [['id' => 1]];
$response = $this->controller->inbox();
$this->assertSame(ResponseInterface::HTTP_OK, $response['code']);
$this->assertSame([['id' => 1]], $response['data']);
}
public function testSendCreatesMessageWhenRecipientProvided()
{
$request = new FakeMessagesRequest([
'recipient_id' => 5,
'subject' => 'Hi',
'message' => 'Hello',
]);
$this->controller->setRequest($request);
$response = $this->controller->send();
$this->assertSame(ResponseInterface::HTTP_OK, $response['code']);
$this->assertSame('Message sent successfully', $response['message']);
$this->assertSame('MSG-TEST', $response['data']['message_number']);
$this->assertNotEmpty($this->controller->messageModel->inserted);
}
public function testGetRecipientsReturnsTeachers()
{
$this->controller->teacherClassModel->data = [
['teacher_id' => 12, 'class_section_id' => 1],
];
$this->controller->userModel->users = [
12 => ['firstname' => 'Jane', 'lastname' => 'Doe'],
];
$response = $this->controller->getRecipients('teacher');
$this->assertSame(ResponseInterface::HTTP_OK, $response['code']);
$this->assertCount(1, $response['data']);
$this->assertSame('Jane Doe', $response['data'][0]['name']);
}
}
@@ -0,0 +1,197 @@
<?php
namespace Tests\App\Controllers\Api;
use App\Controllers\Api\NotificationController;
use CodeIgniter\HTTP\URI;
use CodeIgniter\HTTP\UserAgent;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\Mock\MockIncomingRequest;
use Config\App;
class StubUserNotificationModel
{
public array $whereHistory = [];
private ?array $firstResult = null;
public ?array $lastUpdate = null;
public function select(string $columns): self
{
return $this;
}
public function join(string $table, string $cond): self
{
return $this;
}
public function where(string $column, $value = null): self
{
$this->whereHistory[] = ['column' => $column, 'value' => $value];
return $this;
}
public function first(): ?array
{
return $this->firstResult;
}
public function update(int $id, array $data): bool
{
$this->lastUpdate = ['id' => $id, 'data' => $data];
return true;
}
public function setFirstResult(?array $result): void
{
$this->firstResult = $result;
}
}
class FakeNotificationRequest extends MockIncomingRequest
{
public function __construct(private readonly array $params = [])
{
parent::__construct(config(App::class), new URI('https://test.alrahmaisgl.org'), null, new UserAgent());
}
public function getGet($key = null, $filter = null, $default = null)
{
if ($key === null) {
return $this->params;
}
return $this->params[$key] ?? $default;
}
}
class TestableNotificationController extends NotificationController
{
private array $overridePagination = [];
private ?object $currentUser = null;
public function __construct()
{
// Skip normal boot to keep dependencies under test control.
}
public function setModels(StubUserNotificationModel $model): void
{
$this->userNotificationModel = $model;
}
public function setCurrentUser(object $user): void
{
$this->currentUser = $user;
}
public function setPaginatedResult(array $result): void
{
$this->overridePagination = $result;
}
protected function paginate($source, int $page = 1, int $perPage = 20): array
{
if (!empty($this->overridePagination)) {
return $this->overridePagination;
}
return parent::paginate($source, $page, $perPage);
}
protected function getCurrentUser(): ?object
{
return $this->currentUser;
}
}
class NotificationControllerTest extends CIUnitTestCase
{
private TestableNotificationController $controller;
private StubUserNotificationModel $model;
protected function setUp(): void
{
parent::setUp();
$this->controller = new TestableNotificationController();
$this->model = new StubUserNotificationModel();
$this->controller->setModels($this->model);
$this->controller->setCurrentUser((object) ['id' => 42]);
}
public function testIndexReturnsPaginatedNotifications()
{
$payload = ['data' => [['id' => 1]], 'pagination' => ['total' => 1]];
$this->controller->setPaginatedResult($payload);
$request = new FakeNotificationRequest();
$this->controller->setRequest($request);
$response = $this->controller->index();
$this->assertTrue($response['status']);
$this->assertSame($payload, $response['data']);
$this->assertSame('Notifications retrieved successfully', $response['message']);
}
public function testIndexRespectsReadFilter()
{
$payload = ['data' => [], 'pagination' => ['total' => 0]];
$this->controller->setPaginatedResult($payload);
$request = new FakeNotificationRequest(['read' => 'false']);
$this->controller->setRequest($request);
$this->controller->index();
$readWhere = array_filter(
$this->model->whereHistory,
static fn ($entry) => $entry['column'] === 'user_notifications.is_read'
);
$this->assertNotEmpty($readWhere);
$this->assertSame(0, $readWhere[array_key_last($readWhere)]['value']);
}
public function testShowReturnsNotification()
{
$this->model->setFirstResult(['id' => 99, 'notification_id' => 99]);
$response = $this->controller->show(99);
$this->assertTrue($response['status']);
$this->assertSame('Notification retrieved successfully', $response['message']);
}
public function testShowReturns404WhenMissing()
{
$this->model->setFirstResult(null);
$response = $this->controller->show(9);
$this->assertFalse($response['status']);
$this->assertSame(404, $response['code']);
}
public function testMarkReadUpdatesNotification()
{
$this->model->setFirstResult(['id' => 50, 'notification_id' => 500]);
$response = $this->controller->markRead(500);
$this->assertTrue($response['status']);
$this->assertSame('Notification marked as read', $response['message']);
$this->assertSame(50, $this->model->lastUpdate['id']);
$this->assertSame(1, $this->model->lastUpdate['data']['is_read']);
$this->assertNotEmpty($this->model->lastUpdate['data']['read_at']);
}
public function testMarkReadReturns404WhenNotFound()
{
$this->model->setFirstResult(null);
$response = $this->controller->markRead(77);
$this->assertFalse($response['status']);
$this->assertSame(404, $response['code']);
}
}
@@ -0,0 +1,125 @@
<?php
namespace Tests\App\Controllers\Api;
use App\Controllers\Api\ParentController;
use App\Models\AllergyModel;
use App\Models\ClassSectionModel;
use App\Models\MedicalConditionModel;
use App\Models\StudentClassModel;
use App\Models\StudentModel;
use App\Models\UserModel;
use CodeIgniter\Test\CIUnitTestCase;
class TestableParentController extends ParentController
{
public function __construct()
{
// Avoid real model initialization
}
public function setModels(UserModel $userModel, StudentModel $studentModel, StudentClassModel $studentClassModel, ClassSectionModel $classSectionModel, MedicalConditionModel $medicalConditionModel, AllergyModel $allergyModel): void
{
$this->userModel = $userModel;
$this->studentModel = $studentModel;
$this->studentClassModel = $studentClassModel;
$this->classSectionModel = $classSectionModel;
$this->medicalConditionModel = $medicalConditionModel;
$this->allergyModel = $allergyModel;
}
}
class ParentControllerTest extends CIUnitTestCase
{
private TestableParentController $controller;
private $userModel;
private $studentModel;
private $studentClassModel;
private $classSectionModel;
private $medicalConditionModel;
private $allergyModel;
protected function setUp(): void
{
parent::setUp();
$this->userModel = $this->createMock(UserModel::class);
$this->studentModel = $this->createMock(StudentModel::class);
$this->studentClassModel = $this->createMock(StudentClassModel::class);
$this->classSectionModel = $this->createMock(ClassSectionModel::class);
$this->medicalConditionModel = $this->createMock(MedicalConditionModel::class);
$this->allergyModel = $this->createMock(AllergyModel::class);
$this->controller = new TestableParentController();
$this->controller->setModels(
$this->userModel,
$this->studentModel,
$this->studentClassModel,
$this->classSectionModel,
$this->medicalConditionModel,
$this->allergyModel
);
}
public function testShowReturnsParentById()
{
$this->userModel->method('select')->willReturnSelf();
$this->userModel->method('join')->willReturnSelf();
$this->userModel->method('where')->willReturnSelf();
$this->userModel->method('first')->willReturn(['id' => 7, 'firstname' => 'Sara']);
$response = $this->controller->show(7);
$this->assertTrue($response['status']);
$this->assertEquals('Parent retrieved successfully', $response['message']);
}
public function testShowMissingParentReturnsError()
{
$this->userModel->method('select')->willReturnSelf();
$this->userModel->method('join')->willReturnSelf();
$this->userModel->method('where')->willReturnSelf();
$this->userModel->method('first')->willReturn(null);
$response = $this->controller->show(99);
$this->assertFalse($response['status']);
$this->assertEquals(404, $response['code']);
}
public function testGetStudentsReturnsStudentList()
{
$this->userModel->method('find')->with(5)->willReturn(['id' => 5]);
$this->studentModel->method('where')->willReturnSelf();
$this->studentModel->method('findAll')->willReturn([
['id' => 11, 'firstname' => 'Meera', 'lastname' => 'Saleh'],
]);
$this->studentClassModel->method('select')->willReturnSelf();
$this->studentClassModel->method('join')->willReturnSelf();
$this->studentClassModel->method('where')->willReturnSelf();
$this->studentClassModel->method('orderBy')->willReturnSelf();
$this->studentClassModel->method('first')->willReturn(['id' => 2, 'class_section_id' => 3]);
$this->classSectionModel->method('getClassSectionNameBySectionId')->with(3)->willReturn('Grade 1A');
$this->medicalConditionModel->method('where')->willReturnSelf();
$this->medicalConditionModel->method('findColumn')->willReturn([]);
$this->allergyModel->method('where')->willReturnSelf();
$this->allergyModel->method('findColumn')->willReturn([]);
$response = $this->controller->getStudents(5);
$this->assertTrue($response['status']);
$this->assertNotEmpty($response['data']);
$this->assertEquals('Grade 1A', $response['data'][0]['class_section']);
$this->assertEquals(3, $response['data'][0]['current_class']['class_section_id']);
}
public function testGetStudentsMissingParent()
{
$this->userModel->method('find')->with(9)->willReturn(null);
$response = $this->controller->getStudents(9);
$this->assertFalse($response['status']);
$this->assertEquals(404, $response['code']);
}
}
@@ -0,0 +1,38 @@
<?php
namespace Tests\App\Controllers\Api;
use App\Controllers\Api\PolicyController;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Test\CIUnitTestCase;
class TestablePolicyController extends PolicyController
{
protected function success($data = null, string $message = 'Success', int $code = ResponseInterface::HTTP_OK)
{
return ['data' => $data, 'message' => $message, 'code' => $code];
}
}
class PolicyControllerTest extends CIUnitTestCase
{
public function testSchoolPicturePolicyProducesHtml()
{
$controller = new TestablePolicyController();
$response = $controller->schoolPicturePolicy();
$this->assertSame(ResponseInterface::HTTP_OK, $response['code']);
$this->assertStringContainsString('<div class="policy-text">', $response['data']['html']);
}
public function testSchoolPolicyReturnsMetaAndHtml()
{
$controller = new TestablePolicyController();
$response = $controller->schoolPolicy();
$this->assertSame(ResponseInterface::HTTP_OK, $response['code']);
$this->assertArrayHasKey('meta', $response['data']);
$this->assertArrayHasKey('html', $response['data']);
$this->assertStringContainsString('<!DOCTYPE html>', $response['data']['html']);
}
}
@@ -0,0 +1,250 @@
<?php
namespace Tests\App\Controllers\Api;
use App\Controllers\Api\PreferencesController;
use CodeIgniter\HTTP\URI;
use CodeIgniter\HTTP\UserAgent;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\Mock\MockIncomingRequest;
use Config\App;
use Config\Services;
class StubPreferencesModel
{
private ?array $firstResult = null;
private array $findResults = [];
private int $nextInsertId = 200;
public ?array $lastUpdate = null;
public ?array $lastInsertData = null;
public function where(string $column, $value): self
{
return $this;
}
public function first(): ?array
{
return $this->firstResult;
}
public function setFirstResult(?array $result): void
{
$this->firstResult = $result;
}
public function update(int $id, array $data): bool
{
$this->lastUpdate = ['id' => $id, 'data' => $data];
$this->findResults[$id] = ['id' => $id] + $data;
return true;
}
public function insert(array $data): int
{
$id = $this->nextInsertId++;
$this->lastInsertData = $data;
$this->findResults[$id] = ['id' => $id] + $data;
return $id;
}
public function find(int $id): ?array
{
return $this->findResults[$id] ?? null;
}
public function setFindResult(int $id, array $result): void
{
$this->findResults[$id] = $result;
}
}
class FakeJsonRequest extends MockIncomingRequest
{
public function __construct(private readonly ?array $payload)
{
parent::__construct(config(App::class), new URI('https://test.alrahmaisgl.org'), null, new UserAgent());
}
public function getJSON(bool $assoc = false, int $depth = 512, int $options = 0): ?array
{
return $this->payload;
}
}
class TestablePreferencesController extends PreferencesController
{
private ?object $currentUser = null;
public function __construct()
{
// Skip the real constructor to avoid bootstrapping models.
}
public function setPreferencesModel(StubPreferencesModel $model): void
{
$this->preferencesModel = $model;
}
public function setCurrentUser(?object $user): void
{
$this->currentUser = $user;
}
protected function getCurrentUser(): ?object
{
return $this->currentUser;
}
}
class PreferencesControllerTest extends CIUnitTestCase
{
private TestablePreferencesController $controller;
private StubPreferencesModel $preferencesModel;
protected function setUp(): void
{
parent::setUp();
Services::resetSingle('validation');
Services::resetSingle('session');
$this->preferencesModel = new StubPreferencesModel();
$this->controller = new TestablePreferencesController();
$this->controller->setPreferencesModel($this->preferencesModel);
}
public function testIndexRequiresAuthentication()
{
$this->controller->setCurrentUser(null);
$response = $this->controller->index();
$this->assertFalse($response['status']);
$this->assertSame(401, $response['code']);
$this->assertSame('Authentication required', $response['message']);
}
public function testIndexReturnsDefaultsIfNoPreferences()
{
$this->controller->setCurrentUser((object) ['id' => 7]);
$this->preferencesModel->setFirstResult(null);
$response = $this->controller->index();
$this->assertTrue($response['status']);
$this->assertSame('Preferences retrieved successfully', $response['message']);
$this->assertSame(1, $response['data']['notification_email']);
$this->assertSame('light', $response['data']['theme']);
$this->assertSame('blue', $response['data']['style_color']);
$this->assertSame('white', $response['data']['menu_color']);
}
public function testIndexPropagatesStoredPreferencesAndSessionStyles()
{
$this->controller->setCurrentUser((object) ['id' => 9]);
$this->preferencesModel->setFirstResult([
'id' => 11,
'user_id' => 9,
'notification_email' => 0,
'notification_sms' => 0,
'theme' => 'dark',
'language' => 'fr',
]);
session()->set('style_color', 'rose');
session()->set('menu_color', 'navy');
$response = $this->controller->index();
$this->assertTrue($response['status']);
$this->assertSame(0, $response['data']['notification_email']);
$this->assertSame('rose', $response['data']['style_color']);
$this->assertSame('navy', $response['data']['menu_color']);
}
public function testUpdateReturnsValidationErrors()
{
$this->controller->setCurrentUser((object) ['id' => 3]);
$this->setRequestPayload(['notification_email' => '2']);
$response = $this->controller->update();
$this->assertFalse($response['status']);
$this->assertSame(422, $response['code']);
$this->assertSame('Validation failed', $response['message']);
$this->assertArrayHasKey('errors', $response);
}
public function testUpdateInsertsWhenPreferencesAbsent()
{
$this->controller->setCurrentUser((object) ['id' => 12]);
$this->preferencesModel->setFirstResult(null);
$payload = [
'notification_email' => 0,
'notification_sms' => 1,
'theme' => 'dark',
'language' => 'es',
'style_color' => 'green',
'menu_color' => 'brand',
];
$this->setRequestPayload($payload);
$response = $this->controller->update();
$this->assertTrue($response['status']);
$this->assertSame('Preferences updated successfully', $response['message']);
$this->assertSame(0, $response['data']['notification_email']);
$this->assertSame('green', session()->get('style_color'));
$this->assertSame('brand', session()->get('menu_color'));
$this->assertSame($this->preferencesModel->lastInsertData, [
'user_id' => 12,
'notification_email' => 0,
'notification_sms' => 1,
'theme' => 'dark',
'language' => 'es',
]);
}
public function testUpdateModifiesExistingPreferences()
{
$this->controller->setCurrentUser((object) ['id' => 21]);
$this->preferencesModel->setFirstResult([
'id' => 18,
'user_id' => 21,
'notification_email' => 1,
'notification_sms' => 1,
'theme' => 'light',
'language' => 'en',
]);
$this->preferencesModel->setFindResult(18, [
'id' => 18,
'user_id' => 21,
'notification_email' => 1,
'notification_sms' => 1,
'theme' => 'light',
'language' => 'en',
]);
$payload = ['notification_email' => 1, 'notification_sms' => 0, 'language' => 'fr'];
$this->setRequestPayload($payload);
$response = $this->controller->update();
$this->assertTrue($response['status']);
$this->assertSame('Preferences updated successfully', $response['message']);
$this->assertSame(1, $this->preferencesModel->lastUpdate['data']['notification_email']);
$this->assertSame(0, $this->preferencesModel->lastUpdate['data']['notification_sms']);
$this->assertSame(18, $this->preferencesModel->lastUpdate['id']);
$this->assertSame('fr', $response['data']['language']);
}
private function setRequestPayload(?array $payload): void
{
$request = new FakeJsonRequest($payload);
$this->controller->setRequest($request);
}
}
@@ -0,0 +1,132 @@
<?php
namespace Tests\App\Controllers\Api;
use App\Controllers\Api\RolePermissionController;
use App\Models\PermissionModel;
use App\Models\RoleModel;
use App\Models\RolePermissionModel;
use App\Models\UserModel;
use CodeIgniter\Test\CIUnitTestCase;
class TestableRolePermissionController extends RolePermissionController
{
public function setModels(RoleModel $roleModel, PermissionModel $permissionModel, RolePermissionModel $rolePermissionModel, UserModel $userModel): void
{
$this->roleModel = $roleModel;
$this->permissionModel = $permissionModel;
$this->rolePermissionModel = $rolePermissionModel;
$this->userModel = $userModel;
}
private ?int $currentUserId = 1;
public function setCurrentUserId(int $id): void
{
$this->currentUserId = $id;
}
protected function getCurrentUserId(): ?int
{
return $this->currentUserId;
}
}
class RolePermissionControllerTest extends CIUnitTestCase
{
private $controller;
private $roleModel;
private $permissionModel;
private $rolePermissionModel;
private $userModel;
protected function setUp(): void
{
parent::setUp();
$this->roleModel = $this->createMock(RoleModel::class);
$this->permissionModel = $this->createMock(PermissionModel::class);
$this->rolePermissionModel = $this->createMock(RolePermissionModel::class);
$this->userModel = $this->createMock(UserModel::class);
$this->controller = new TestableRolePermissionController();
$this->controller->setModels($this->roleModel, $this->permissionModel, $this->rolePermissionModel, $this->userModel);
}
public function testRolesListReturnsData()
{
$this->roleModel->method('findAll')->willReturn([
['id' => 1, 'name' => 'admin'],
]);
$response = $this->controller->roles();
$this->assertTrue($response['status']);
$this->assertNotEmpty($response['data']);
}
public function testPermissionsListReturnsData()
{
$this->permissionModel->method('findAll')->willReturn([
['id' => 11, 'name' => 'view_reports'],
]);
$response = $this->controller->permissions();
$this->assertTrue($response['status']);
$this->assertNotEmpty($response['data']);
}
public function testRolePermissionsReturnsDataForRole()
{
$this->rolePermissionModel->method('where')->with('role_id', 2)->willReturnSelf();
$this->rolePermissionModel->method('findAll')->willReturn([
['permission_id' => 5],
]);
$response = $this->controller->rolePermissions(2);
$this->assertTrue($response['status']);
$this->assertEquals('Role permissions retrieved successfully', $response['message']);
}
public function testUsersListReturnsSummaries()
{
$this->userModel->method('select')->willReturnSelf();
$this->userModel->method('findAll')->willReturn([
['id' => 3, 'firstname' => 'Ali', 'lastname' => 'Khan'],
]);
$response = $this->controller->users();
$this->assertTrue($response['status']);
$this->assertCount(1, $response['data']);
}
public function testAssignRoleSuccess()
{
$dbMock = $this->createMock(\CodeIgniter\Database\BaseConnection::class);
$dbMock->method('transStart')->willReturn(null);
$dbMock->method('transComplete')->willReturn(null);
$dbMock->method('transStatus')->willReturn(true);
if (!class_exists('Config\\FakeDatabase')) {
eval('namespace Config; class FakeDatabase extends \\Config\\Database {
public static function connect($group = null, bool $getShared = true) {
return $GLOBALS["__fakeDb__"];
}
}');
}
$GLOBALS['__fakeDb__'] = $dbMock;
class_alias('Config\\FakeDatabase', 'Config\\Database');
$this->userRoleModel->method('where')->with('user_id', 7)->willReturnSelf();
$this->userRoleModel->method('delete')->willReturn(true);
$this->userRoleModel->method('insert')->willReturn(true);
$this->controller->setCurrentUserId(12);
$req = $this->makeRequest('POST', '/api/v1/users/7/roles', ['role_ids' => [1, 2]]);
$this->controller->setRequest($req);
$response = $this->controller->assignRole(7);
$this->assertTrue($response['status']);
$this->assertEquals('Roles assigned successfully', $response['message']);
}
}
@@ -0,0 +1,240 @@
<?php
namespace Tests\App\Controllers\Api;
use App\Controllers\Api\ScoreCommentController;
use CodeIgniter\HTTP\URI;
use CodeIgniter\HTTP\UserAgent;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\Mock\MockIncomingRequest;
use Config\App;
use Config\Services;
class FakeScoreRequest extends MockIncomingRequest
{
public function __construct(private readonly ?array $json, 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 StubScoreCommentModel
{
private array $existingQueue = [];
public array $savedRecords = [];
public array $lastUpdate = [];
private int $lastInsert = 100;
public array $findAllResult = [];
public function where($column, $value = null): self
{
return $this;
}
public function first(): ?array
{
return array_shift($this->existingQueue) ?? null;
}
public function addExisting(array $row): void
{
$this->existingQueue[] = $row;
}
public function insert(array $data): int
{
$this->lastInsert++;
$id = $this->lastInsert;
$this->savedRecords[$id] = ['id' => $id] + $data;
return $id;
}
public function update(int $id, array $data): bool
{
$this->savedRecords[$id] = ['id' => $id] + $data;
$this->lastUpdate = ['id' => $id, 'data' => $data];
return true;
}
public function find(int $id): ?array
{
return $this->savedRecords[$id] ?? null;
}
public function setFind(int $id, array $row): void
{
$this->savedRecords[$id] = $row;
}
public function setFindAllResult(array $result): void
{
$this->findAllResult = $result;
}
public function findAll(): array
{
return $this->findAllResult;
}
}
class TestableScoreCommentController extends ScoreCommentController
{
private ?object $currentUser = null;
public function __construct()
{
// bypass real constructor logic.
}
public function setScoreCommentModel(StubScoreCommentModel $model): void
{
$this->scoreCommentModel = $model;
}
public function setCurrentUser(?object $user): void
{
$this->currentUser = $user;
}
public function setAcademicPeriod(string $schoolYear, string $semester): void
{
$this->schoolYear = $schoolYear;
$this->semester = $semester;
}
protected function getCurrentUser(): ?object
{
return $this->currentUser;
}
}
class ScoreCommentControllerTest extends CIUnitTestCase
{
private TestableScoreCommentController $controller;
private StubScoreCommentModel $model;
protected function setUp(): void
{
parent::setUp();
Services::resetSingle('validation');
$this->controller = new TestableScoreCommentController();
$this->model = new StubScoreCommentModel();
$this->controller->setScoreCommentModel($this->model);
$this->controller->setAcademicPeriod('2024-2025', 'Spring');
}
public function testSaveReturnsErrorWhenNoData()
{
$request = new FakeScoreRequest(null);
$this->controller->setRequest($request);
$this->controller->setCurrentUser((object) ['id' => 4]);
$response = $this->controller->save();
$this->assertFalse($response['status']);
$this->assertSame(400, $response['code']);
$this->assertSame('Invalid request data', $response['message']);
}
public function testSaveInsertsCommentsSkippingEmpty()
{
$request = new FakeScoreRequest([
'subject_id' => 7,
'comments' => [
101 => ['Exam' => 'Well done', 'Project' => ' '],
],
]);
$this->controller->setRequest($request);
$this->controller->setCurrentUser((object) ['id' => 22]);
$response = $this->controller->save();
$this->assertTrue($response['status']);
$this->assertSame('Comments saved successfully', $response['message']);
$this->assertCount(1, $response['data']);
$saved = $response['data'][0];
$this->assertSame('Well done', $saved['comment']);
$this->assertSame('exam', $saved['score_type']);
}
public function testSaveUpdatesExistingComment()
{
$existing = [
'id' => 55,
'student_id' => 201,
'score_type' => 'Exam',
];
$this->model->addExisting($existing);
$this->model->setFind(55, ['id' => 55, 'comment' => 'Old']);
$request = new FakeScoreRequest([
'subject_id' => 7,
'comments' => [
201 => ['Exam' => 'Better now'],
],
'semester' => 'Fall',
]);
$this->controller->setRequest($request);
$this->controller->setCurrentUser((object) ['id' => 9]);
$response = $this->controller->save();
$this->assertTrue($response['status']);
$this->assertSame(55, $this->model->lastUpdate['id']);
$this->assertSame('Better now', $this->model->lastUpdate['data']['comment']);
$this->assertSame('Fall', $this->model->lastUpdate['data']['semester']);
}
public function testGetByStudentReturnsComments()
{
$this->controller->setRequest(new FakeScoreRequest(null, ['subject_id' => 5, 'score_type' => 'exam']));
$this->model->setFindAllResult([['id' => 1, 'student_id' => 7]]);
$response = $this->controller->getByStudent(7);
$this->assertTrue($response['status']);
$this->assertCount(1, $response['data']);
$this->assertSame('Comments retrieved successfully', $response['message']);
}
public function testUpdateReturnsNotFoundWhenMissing()
{
$this->model->setFindAllResult([]);
$request = new FakeScoreRequest([]);
$this->controller->setRequest($request);
$response = $this->controller->update(99);
$this->assertFalse($response['status']);
$this->assertSame(404, $response['code']);
}
public function testUpdateModifiesComment()
{
$this->model->setFind(42, ['id' => 42, 'comment' => 'Old']);
$request = new FakeScoreRequest(['comment' => 'Fresh']);
$this->controller->setRequest($request);
$response = $this->controller->update(42);
$this->assertTrue($response['status']);
$this->assertSame('Fresh', $this->model->lastUpdate['data']['comment']);
$this->assertSame('Comment updated successfully', $response['message']);
}
}
@@ -0,0 +1,130 @@
<?php
namespace Tests\App\Controllers\Api;
use App\Controllers\Api\ScorePredictor;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Test\CIUnitTestCase;
class ScorePredictorStubBuilder
{
private string $name;
public function __construct(string $name)
{
$this->name = $name;
}
public function select(...$args)
{
return $this;
}
public function join(...$args)
{
return $this;
}
public function where(...$args)
{
return $this;
}
public function groupBy(...$args)
{
return $this;
}
public function get()
{
return $this;
}
public function getResultArray(): array
{
return [];
}
public function getRow(): object
{
return (object)['mean' => 70, 'std' => 10];
}
}
class ScorePredictorStubDatabase
{
public function connect()
{
return true;
}
public function escape($value)
{
return "'" . addslashes((string)$value) . "'";
}
public function table(string $name)
{
return new ScorePredictorStubBuilder($name);
}
}
class StubConfigurationModel
{
public function getConfig($key)
{
return match ($key) {
'semester' => 'Fall',
'school_year' => '2025-2026',
'trophy_score' => 95,
'pass_score' => 60,
default => null,
};
}
}
class StubClassSectionModel
{
public function findAll(): array
{
return [];
}
}
class TestableScorePredictor extends ScorePredictor
{
public function __construct()
{
$this->db = new ScorePredictorStubDatabase();
$this->configModel = new StubConfigurationModel();
$this->classSectionModel = new StubClassSectionModel();
$this->semester = 'Fall';
$this->schoolYear = '2025-2026';
$this->targetTrophy = 95;
$this->targetLow = 60;
}
protected function success($data = null, string $message = 'Success', int $code = ResponseInterface::HTTP_OK)
{
return ['data' => $data, 'message' => $message, 'code' => $code];
}
protected function error(string $message = 'An error occurred', int $code = ResponseInterface::HTTP_BAD_REQUEST, ?array $errors = null)
{
return ['message' => $message, 'code' => $code, 'errors' => $errors];
}
}
class ScorePredictorTest extends CIUnitTestCase
{
public function testCombinedReportReturnsSuccess()
{
$controller = new TestableScorePredictor();
$response = $controller->combinedReport();
$this->assertSame(ResponseInterface::HTTP_OK, $response['code']);
$this->assertArrayHasKey('students', $response['data']);
$this->assertArrayHasKey('class_sections', $response['data']);
$this->assertSame('Score prediction generated', $response['message']);
}
}
@@ -0,0 +1,61 @@
<?php
namespace Tests\App\Controllers\Api;
use App\Controllers\Api\SessionTimeoutController;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Test\CIUnitTestCase;
class TestableSessionTimeoutController extends SessionTimeoutController
{
protected function success($data = null, string $message = 'Success', int $code = ResponseInterface::HTTP_OK)
{
return ['data' => $data, 'message' => $message, 'code' => $code];
}
protected function error(string $message = 'An error occurred', int $code = ResponseInterface::HTTP_BAD_REQUEST, ?array $errors = null)
{
return ['message' => $message, 'code' => $code, 'errors' => $errors];
}
}
class SessionTimeoutControllerTest extends CIUnitTestCase
{
protected function tearDown(): void
{
parent::tearDown();
session()->destroy();
}
public function testGetTimeoutConfigIncludesKeys()
{
$controller = new TestableSessionTimeoutController();
$response = $controller->getTimeoutConfig();
$this->assertSame(ResponseInterface::HTTP_OK, $response['code']);
$this->assertArrayHasKey('timeout', $response['data']);
$this->assertArrayHasKey('check_url', $response['data']);
}
public function testCheckTimeoutReturnsActiveWhenFresh()
{
session()->set('last_activity', time());
$controller = new TestableSessionTimeoutController();
$response = $controller->checkTimeout();
$this->assertSame(ResponseInterface::HTTP_OK, $response['code']);
$this->assertSame('Session active', $response['message']);
$this->assertSame('active', $response['data']['status']);
}
public function testPingActivityRefreshesSession()
{
$controller = new TestableSessionTimeoutController();
session()->set('last_activity', time());
$response = $controller->pingActivity();
$this->assertSame(ResponseInterface::HTTP_OK, $response['code']);
$this->assertSame('Session activity refreshed', $response['message']);
$this->assertSame('active', $response['data']['status']);
}
}
@@ -0,0 +1,89 @@
<?php
namespace Tests\App\Controllers\Api;
use App\Controllers\Api\SupplierController;
use App\Models\SupplierModel;
use CodeIgniter\Test\CIUnitTestCase;
class TestableSupplierController extends SupplierController
{
public function setSupplierModel(SupplierModel $model): void
{
$this->supplierModel = $model;
}
}
class SupplierControllerTest extends CIUnitTestCase
{
private $controller;
private $model;
protected function setUp(): void
{
parent::setUp();
$this->model = $this->createMock(SupplierModel::class);
$this->controller = new TestableSupplierController();
$this->controller->setSupplierModel($this->model);
}
public function testShowReturnsNotFound()
{
$this->model->method('find')->with(99)->willReturn(null);
$response = $this->controller->show(99);
$this->assertFalse($response['status']);
$this->assertEquals(404, $response['code']);
}
public function testCreateValidSupplier()
{
$payload = [
'name' => 'Campus Supplies',
'email' => 'supplies@example.com',
];
$req = $this->makeRequest('POST', '/api/v1/suppliers', $payload);
$this->controller->setRequest($req);
$this->model->method('insert')->with($this->arrayHasKey('name'))->willReturn(101);
$this->model->method('find')->with(101)->willReturn(['id' => 101, 'name' => 'Campus Supplies']);
$response = $this->controller->create();
$this->assertTrue($response['status']);
$this->assertEquals('Supplier created successfully', $response['message']);
$this->assertEquals(101, $response['data']['id']);
}
public function testUpdateSupplierPersistsFields()
{
$current = ['id' => 22, 'name' => 'Old Name'];
$this->model->method('find')->willReturnCallback(fn($id) => $current);
$this->model->method('update')->willReturnCallback(function($id, $data) use (&$current) {
$current = array_merge($current, $data);
return true;
});
$req = $this->makeRequest('PUT', '/api/v1/suppliers/22', [
'name' => 'New Vendor',
'phone' => '555-0199',
]);
$this->controller->setRequest($req);
$response = $this->controller->update(22);
$this->assertTrue($response['status']);
$this->assertEquals('Supplier updated successfully', $response['message']);
$this->assertEquals('New Vendor', $response['data']['name']);
}
public function testDeleteSupplier()
{
$this->model->method('find')->with(33)->willReturn(['id' => 33]);
$this->model->method('delete')->with(33)->willReturn(true);
$response = $this->controller->delete(33);
$this->assertTrue($response['status']);
$this->assertEquals('Supplier deleted successfully', $response['message']);
}
}
@@ -0,0 +1,207 @@
<?php
namespace Tests\App\Controllers\Api;
use App\Controllers\Api\TeacherController;
use App\Models\ConfigurationModel;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\Test\CIUnitTestCase;
class StubUserModel
{
private ?array $firstResult = null;
public function select(string $columns): self
{
return $this;
}
public function join(string $table, string $cond, ?string $type = null): self
{
return $this;
}
public function whereIn(string $column, array $values): self
{
return $this;
}
public function groupBy(string $column): self
{
return $this;
}
public function where(string $column, $value = null): self
{
return $this;
}
public function first(): ?array
{
return $this->firstResult;
}
public function setFirstResult(?array $result): void
{
$this->firstResult = $result;
}
}
class StubTeacherClassModel
{
private array $result = [];
public function select(string $columns): self
{
return $this;
}
public function join(string $table, string $cond, ?string $type = null): self
{
return $this;
}
public function where(string $column, $value = null): self
{
return $this;
}
public function findAll(): array
{
return $this->result;
}
public function setResult(array $result): void
{
$this->result = $result;
}
}
class TestableTeacherController extends TeacherController
{
private array $overridePagination = [];
public function __construct()
{
// Prevent the real controller bootstrapping from hitting the database.
}
public function setModels($userModel, $teacherClassModel, ConfigurationModel $configModel): void
{
$this->userModel = $userModel;
$this->teacherClassModel = $teacherClassModel;
$this->configModel = $configModel;
}
public function setPaginatedResult(array $result): void
{
$this->overridePagination = $result;
}
protected function paginate($source, int $page = 1, int $perPage = 20): array
{
if (!empty($this->overridePagination)) {
return $this->overridePagination;
}
return parent::paginate($source, $page, $perPage);
}
}
class TeacherControllerTest extends CIUnitTestCase
{
private TestableTeacherController $controller;
private $userModel;
private $teacherClassModel;
private $configModel;
protected function setUp(): void
{
parent::setUp();
$this->userModel = new StubUserModel();
$this->teacherClassModel = new StubTeacherClassModel();
$this->configModel = $this->createMock(ConfigurationModel::class);
$this->configModel->method('getConfig')->willReturnMap([
['school_year', '2024-2025'],
['semester', 'Spring'],
]);
$this->controller = new TestableTeacherController();
$request = $this->createMock(IncomingRequest::class);
$request->method('getGet')->willReturn(null);
$this->controller->setRequest($request);
$this->controller->setModels($this->userModel, $this->teacherClassModel, $this->configModel);
}
public function testIndexSkipsSensitiveFieldsAndAddsAssignments()
{
$paginatedPayload = [
'data' => [
['id' => 5, 'email' => 'vera@example.com', 'password' => 'secret', 'token' => 'x'],
],
'pagination' => ['current_page' => 1, 'per_page' => 20, 'total' => 1, 'total_pages' => 1],
];
$assignments = [['class_section_id' => 7, 'school_year' => '2024-2025', 'semester' => 'Spring']];
$this->teacherClassModel->setResult($assignments);
$this->controller->setPaginatedResult($paginatedPayload);
$response = $this->controller->index();
$this->assertTrue($response['status']);
$this->assertSame('Teachers retrieved successfully', $response['message']);
$teachers = $response['data']['data'];
$this->assertCount(1, $teachers);
$teacher = $teachers[0];
$this->assertArrayNotHasKey('password', $teacher);
$this->assertArrayNotHasKey('token', $teacher);
$this->assertSame($assignments, $teacher['class_assignments']);
}
public function testShowReturnsTeacherWithAssignments()
{
$teacherRow = ['id' => 8, 'email' => 'nora@example.com', 'password' => 'secret', 'token' => 'abc'];
$this->userModel->setFirstResult($teacherRow);
$assignments = [['class_section_id' => 9]];
$this->teacherClassModel->setResult($assignments);
$response = $this->controller->show(8);
$this->assertTrue($response['status']);
$this->assertSame('Teacher retrieved successfully', $response['message']);
$teacher = $response['data'];
$this->assertArrayNotHasKey('password', $teacher);
$this->assertArrayNotHasKey('token', $teacher);
$this->assertSame($assignments, $teacher['class_assignments']);
}
public function testShowReturns404WhenTeacherMissing()
{
$this->userModel->setFirstResult(null);
$response = $this->controller->show(99);
$this->assertFalse($response['status']);
$this->assertSame(404, $response['code']);
$this->assertSame('Teacher not found', $response['message']);
}
public function testGetClassesReturnsAssignments()
{
$assignments = [['class_section_id' => 3]];
$this->teacherClassModel->setResult($assignments);
$response = $this->controller->getClasses(4);
$this->assertTrue($response['status']);
$this->assertSame('Teacher classes retrieved successfully', $response['message']);
$this->assertSame($assignments, $response['data']);
}
}
@@ -0,0 +1,54 @@
<?php
namespace Tests\App\Controllers\Api;
use App\Controllers\Api\TestDBController;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Test\CIUnitTestCase;
class StubQuery
{
public function getRow()
{
return (object)['result' => 1];
}
}
class StubDatabase
{
public function query(string $sql)
{
return new StubQuery();
}
}
class TestableTestDBController extends TestDBController
{
protected function success($data = null, string $message = 'Success', int $code = ResponseInterface::HTTP_OK)
{
return ['data' => $data, 'message' => $message, 'code' => $code];
}
protected function error(string $message = 'An error occurred', int $code = ResponseInterface::HTTP_BAD_REQUEST, ?array $errors = null)
{
return ['message' => $message, 'code' => $code, 'errors' => $errors];
}
protected function getDatabaseConnection()
{
return new StubDatabase();
}
}
class TestDBControllerTest extends CIUnitTestCase
{
public function testIndexReturnsSuccessWhenDatabaseOk()
{
$controller = new TestableTestDBController();
$response = $controller->index();
$this->assertSame(ResponseInterface::HTTP_OK, $response['code']);
$this->assertSame('Database check completed', $response['message']);
$this->assertSame('Database connection is successful.', $response['data']['message']);
}
}
@@ -0,0 +1,136 @@
<?php
namespace Tests\App\Controllers\Api;
use App\Controllers\Api\UiController;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\URI;
use CodeIgniter\HTTP\UserAgent;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\Mock\MockIncomingRequest;
use Config\App;
use Config\Services;
class FakeJsonRequest extends MockIncomingRequest
{
public function __construct(private readonly ?array $payload)
{
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->payload;
}
}
class TestableUiController extends UiController
{
private ?object $currentUser = null;
private ?IncomingRequest $requestOverride = null;
public function setCurrentUser(?object $user): void
{
$this->currentUser = $user;
}
public function setJsonRequest(FakeJsonRequest $request): void
{
$this->requestOverride = $request;
$this->setRequest($request);
}
protected function getCurrentUser(): ?object
{
return $this->currentUser;
}
}
class UiControllerTest extends CIUnitTestCase
{
private TestableUiController $controller;
protected function setUp(): void
{
parent::setUp();
Services::resetSingle('session');
$this->controller = new TestableUiController();
}
public function testGetStyleRequiresAuthentication()
{
$response = $this->controller->getStyle();
$this->assertFalse($response['status']);
$this->assertSame(401, $response['code']);
}
public function testGetStyleReturnsSessionColors()
{
$session = Services::session();
$session->set([
'style_color' => 'green',
'menu_color' => 'brand',
'menu_custom_bg' => '#123456',
'menu_custom_text' => '#ffffff',
'menu_custom_mode' => 'dark',
]);
$this->controller->setCurrentUser((object) ['id' => 5]);
$response = $this->controller->getStyle();
$this->assertTrue($response['status']);
$this->assertSame('green', $response['data']['style_color']);
$this->assertArrayHasKey('available_palettes', $response['data']);
}
public function testUpdateStyleInvalidJsonReturnsBadRequest()
{
$this->controller->setCurrentUser((object) ['id' => 10]);
$request = new FakeJsonRequest(null);
$this->controller->setJsonRequest($request);
$response = $this->controller->updateStyle();
$this->assertFalse($response['status']);
$this->assertSame(400, $response['code']);
}
public function testUpdateStyleChangesAccentAndMenu()
{
$this->controller->setCurrentUser((object) ['id' => 10]);
$request = new FakeJsonRequest([
'accent' => 'blue',
'menu' => 'sky',
]);
$this->controller->setJsonRequest($request);
$response = $this->controller->updateStyle();
$this->assertTrue($response['status']);
$this->assertSame('sky', $response['data']['menu_color']);
$this->assertSame('blue', $response['data']['style_color']);
}
public function testUpdateStyleHandlesCustomMenu()
{
$this->controller->setCurrentUser((object) ['id' => 10]);
$request = new FakeJsonRequest([
'accent' => 'purple',
'menu' => 'custom',
'menu_bg' => 'ffffff',
'menu_text' => '000000',
'menu_mode' => 'auto',
]);
$this->controller->setJsonRequest($request);
$response = $this->controller->updateStyle();
$this->assertTrue($response['status']);
$this->assertSame('custom', $response['data']['menu_color']);
$this->assertSame('#FFFFFF', session()->get('menu_custom_bg'));
$this->assertSame('#000000', session()->get('menu_custom_text'));
}
}
@@ -0,0 +1,134 @@
<?php
namespace Tests\App\Controllers;
use App\Controllers\AuthController;
use App\Models\IpAttemptModel;
use App\Models\LoginActivityModel;
use App\Models\UserModel;
use CodeIgniter\Database\BaseBuilder;
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Test\CIUnitTestCase;
class TestableAuthController extends AuthController
{
public function __construct()
{
// Skip the real constructor to avoid bootstrapping live models
}
public function setRequest(RequestInterface $request): self
{
$this->request = $request;
return $this;
}
public function setResponse(ResponseInterface $response): self
{
$this->response = $response;
return $this;
}
public function setUserModel(UserModel $model): self
{
$this->userModel = $model;
return $this;
}
public function setIpAttemptModel(IpAttemptModel $model): self
{
$this->ipAttemptModel = $model;
return $this;
}
public function setLoginActivityModel(LoginActivityModel $model): self
{
$this->loginActivityModel = $model;
return $this;
}
}
class AuthControllerTest extends CIUnitTestCase
{
private TestableAuthController $controller;
private UserModel $userModel;
private IpAttemptModel $ipAttemptModel;
private LoginActivityModel $loginActivityModel;
protected function setUp(): void
{
parent::setUp();
$this->controller = new TestableAuthController();
$this->controller->setResponse(service('response'));
$this->userModel = $this->createMock(UserModel::class);
$this->ipAttemptModel = $this->createMock(IpAttemptModel::class);
$this->loginActivityModel = $this->createMock(LoginActivityModel::class);
$this->controller->setUserModel($this->userModel);
$this->controller->setIpAttemptModel($this->ipAttemptModel);
$this->controller->setLoginActivityModel($this->loginActivityModel);
$builder = $this->createMock(BaseBuilder::class);
$builder->method('select')->willReturnSelf();
$builder->method('join')->willReturnSelf();
$builder->method('where')->willReturnSelf();
$builder->method('get')->willReturnSelf();
$builder->method('getResultArray')->willReturn([
['name' => 'admin'],
]);
$fakeDb = $this->createMock(BaseConnection::class);
$fakeDb->method('table')->willReturn($builder);
if (! class_exists('Config\\FakeDatabase')) {
eval('namespace Config; class FakeDatabase extends \\Config\\Database { public static function connect($group = null, bool $getShared = true) { return $GLOBALS["__fakeDb__"]; } }');
}
$GLOBALS['__fakeDb__'] = $fakeDb;
class_alias('Config\\FakeDatabase', 'Config\\Database');
}
public function testApiLoginReturnsTokenAndRoles()
{
$payload = [
'email' => 'admin@example.com',
'password' => '12345678',
];
$user = [
'id' => 16,
'email' => $payload['email'],
'firstname' => 'Admin',
'lastname' => 'Panel',
'password' => pbkdf2_hash($payload['password']),
'is_suspended' => false,
'user_type' => 'admin',
];
$this->ipAttemptModel->method('where')->willReturnSelf();
$this->ipAttemptModel->method('first')->willReturn(null);
$this->userModel->method('where')->willReturnSelf();
$this->userModel->method('first')->willReturn($user);
$this->userModel->method('update')->willReturn(true);
$this->loginActivityModel->method('insert')->willReturn(true);
$this->controller->setRequest($this->makeRequest('POST', '/api/v1/login', $payload));
$response = $this->controller->apiLogin();
$body = json_decode((string) $response->getBody(), true);
$this->assertIsArray($body);
$this->assertTrue($body['status']);
$this->assertArrayHasKey('token', $body);
$this->assertArrayHasKey('user', $body);
$this->assertSame($user['id'], $body['user']['id']);
$this->assertArrayHasKey('roles', $body['user']);
$this->assertTrue($body['user']['roles']['admin'] ?? false);
$this->assertIsString($body['token']);
}
}
@@ -0,0 +1,257 @@
<?php
namespace {
if (!function_exists('view')) {
function view($name, array $data = [], $options = [])
{
return ['view' => $name, 'data' => $data, 'options' => $options];
}
}
if (!function_exists('local_date')) {
function local_date($timestamp, $format)
{
if ($timestamp instanceof \DateTimeInterface) {
return $timestamp->format($format);
}
if (is_numeric($timestamp)) {
return date($format, (int) $timestamp);
}
return date($format);
}
}
}
namespace Tests\App\Controllers\View {
use App\Controllers\View\AdministratorController;
use App\Models\UserModel;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\Test\CIUnitTestCase;
use Config\Services;
class DummyPostRequest
{
public function __construct(private array $post = [])
{
}
public function getPost($key = null)
{
if ($key === null) {
return $this->post;
}
return $this->post[$key] ?? null;
}
}
class FakeRenderer
{
public array $lastData = [];
public string $lastName = '';
public function setData(array $data, string $type = 'raw')
{
$this->lastData = $data;
return $this;
}
public function render(string $name, array $options = [], bool $saveData = false)
{
$this->lastName = $name;
return 'fake-render';
}
}
class FakeAttendanceModel
{
public function __construct(private array $existing = [])
{
}
public function where(...$args)
{
return $this;
}
public function orderBy(...$args)
{
return $this;
}
public function findAll()
{
return $this->existing;
}
public function upsertOne(...$args)
{
return true;
}
}
class TestableAdministratorController extends AdministratorController
{
public function __construct()
{
// Skip expensive parent constructor to avoid real DB/models.
}
public function setUserModel(UserModel $model): self
{
$this->userModel = $model;
return $this;
}
public function setStaffAttendanceModel($model): self
{
$this->staffAttendanceModel = $model;
return $this;
}
public function setSemester(string $semester): self
{
$this->semester = $semester;
return $this;
}
public function setSchoolYear(string $schoolYear): self
{
$this->schoolYear = $schoolYear;
return $this;
}
public function setRequest($request): self
{
$this->request = $request;
return $this;
}
}
class AdministratorControllerTest extends CIUnitTestCase
{
private TestableAdministratorController $controller;
private FakeRenderer $renderer;
protected function setUp(): void
{
parent::setUp();
Services::resetSingle('session');
helper('date');
session()->start();
$this->controller = new TestableAdministratorController();
$this->controller->setSemester('Fall')->setSchoolYear('2024-2025');
$this->renderer = new FakeRenderer();
Services::injectMock('renderer', $this->renderer);
}
protected function tearDown(): void
{
session()->destroy();
parent::tearDown();
}
public function testAbsenceFormRedirectsWhenNotLoggedIn()
{
session()->remove('user_id');
$response = $this->controller->absenceFormAdmin();
$this->assertInstanceOf(RedirectResponse::class, $response);
$this->assertStringContainsString('/login', $response->getHeaderLine('Location') ?: '');
}
public function testAbsenceFormBuildsViewWithAttendance()
{
session()->set('user_id', 7);
$userModel = $this->createMock(UserModel::class);
$userModel->method('find')->with(7)->willReturn([
'firstname' => 'Alex',
'lastname' => 'Admin',
]);
$attendance = new FakeAttendanceModel([
['date' => '2024-12-01'],
]);
$this->controller->setUserModel($userModel)->setStaffAttendanceModel($attendance);
$result = $this->controller->absenceFormAdmin();
$this->assertSame('fake-render', $result);
$this->assertSame('administrator/absence_vacation', $this->renderer->lastName);
$data = $this->renderer->lastData;
$this->assertSame('Alex Admin', $data['admin_name']);
$this->assertSame('Fall', $data['semester']);
$this->assertSame('2024-2025', $data['schoolYear']);
$this->assertSame([['date' => '2024-12-01']], $data['existing']);
$this->assertIsArray($data['availableDates']);
}
public function testSubmitAbsenceRejectsMissingReason()
{
session()->set('user_id', 9);
$req = new DummyPostRequest([
'dates' => ['2024-12-01'],
'reason_type' => '',
'reason' => '',
]);
$this->controller->setRequest($req);
$response = $this->controller->submitAbsenceAdmin();
$this->assertInstanceOf(RedirectResponse::class, $response);
$this->assertStringContainsString('/administrator/absence', $response->getHeaderLine('Location') ?: '');
$this->assertSame('Reason is required.', session()->getFlashdata('message'));
}
public function testSubmitAbsenceSavesAndRedirects()
{
session()->set('user_id', 10);
$userModel = $this->createMock(UserModel::class);
$userModel->method('getUserRole')->with(10)->willReturn('administrator');
$userModel->method('find')->with(10)->willReturn([
'firstname' => 'Sam',
'lastname' => 'Admin',
'email' => 'sam@school.test',
]);
$attendance = new FakeAttendanceModel();
$this->controller->setUserModel($userModel)->setStaffAttendanceModel($attendance);
$method = new \ReflectionMethod($this->controller, 'allowedAbsenceDates');
$method->setAccessible(true);
$allowed = $method->invoke($this->controller);
if (empty($allowed)) {
$this->markTestSkipped('No allowed absence dates are currently available.');
}
$date = $allowed[0];
$req = new DummyPostRequest([
'dates' => [$date],
'reason_type' => 'Personal',
'reason' => 'Conference',
]);
$this->controller->setRequest($req);
$emailService = $this->createMock(\App\Services\EmailService::class);
$emailService->method('send')->willReturn(true);
Services::injectMock('emailService', $emailService);
$response = $this->controller->submitAbsenceAdmin();
$this->assertInstanceOf(RedirectResponse::class, $response);
$this->assertStringContainsString('/administrator/absence', $response->getHeaderLine('Location') ?: '');
$this->assertSame('success', session()->getFlashdata('status'));
$this->assertStringContainsString('saved', (string) session()->getFlashdata('message'));
}
}
}
@@ -0,0 +1,82 @@
<?php
namespace Tests\App\Controllers\View {
use App\Controllers\View\RegisterController;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\Test\CIUnitTestCase;
use Config\Services;
class TestableRegisterController extends RegisterController
{
public function __construct()
{
// Skip the heavy constructor that connects to the real database.
}
}
class RegisterControllerTest extends CIUnitTestCase
{
private TestableRegisterController $controller;
protected function setUp(): void
{
parent::setUp();
Services::resetSingle('session');
session()->start();
$this->controller = new TestableRegisterController();
}
protected function tearDown(): void
{
session()->remove('captcha_answer');
session()->remove('user_email');
parent::tearDown();
}
public function testIndexGeneratesCaptchaWhenMissing()
{
session()->remove('captcha_answer');
$response = $this->controller->index();
$this->assertIsString($response);
$captcha = session()->get('captcha_answer');
$this->assertNotEmpty($captcha);
$this->assertGreaterThanOrEqual(4, strlen($captcha));
$this->assertLessThanOrEqual(8, strlen($captcha));
}
public function testIndexReusesExistingCaptcha()
{
session()->set('captcha_answer', 'EXISTING');
$response = $this->controller->index();
$this->assertIsString($response);
$this->assertSame('EXISTING', session()->get('captcha_answer'));
}
public function testSuccessRedirectsWhenEmailMissing()
{
session()->remove('user_email');
$response = $this->controller->success();
$this->assertInstanceOf(RedirectResponse::class, $response);
$this->assertStringContainsString('/register', $response->getHeaderLine('Location') ?: '');
}
public function testSuccessReturnsViewWhenEmailPresent()
{
session()->set('user_email', 'parent@example.com');
$response = $this->controller->success();
$this->assertIsString($response);
$this->assertSame('parent@example.com', session()->get('user_email'));
}
}
}
@@ -0,0 +1,142 @@
<?php
namespace {
if (!function_exists('base_url')) {
function base_url($uri = '')
{
return 'https://test.alrahmaisgl.org/' . ltrim((string)$uri, '/');
}
}
if (!function_exists('site_url')) {
function site_url($uri = '')
{
return 'https://test.alrahmaisgl.org/' . ltrim((string)$uri, '/');
}
}
if (!function_exists('view')) {
function view($name, $data = [], $options = [])
{
return ['view' => $name, 'data' => $data, 'options' => $options];
}
}
}
namespace Tests\App\Controllers\View {
use App\Controllers\View\ReimbursementController;
use CodeIgniter\Database\BaseBuilder;
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\Database\BaseResult;
use CodeIgniter\Test\CIUnitTestCase;
use Config\Services;
class TestableReimbursementController extends ReimbursementController
{
public function __construct()
{
// Skip the real parent constructor so we can inject mocks.
}
public function setDb(BaseConnection $db): self
{
$this->db = $db;
return $this;
}
}
class FakeRenderer
{
private array $data = [];
private array $lastRender = [];
public function setData(?array $data = null): self
{
$this->data = $data ?? [];
return $this;
}
public function render(string $view, array $data = [], $options = null)
{
$this->lastRender = [
'view' => $view,
'data' => $data ?: $this->data,
'options' => $options,
];
return 'fake-rendered:' . $view;
}
public function getLastRender(): array
{
return $this->lastRender;
}
}
class ReimbursementControllerTest extends CIUnitTestCase
{
private TestableReimbursementController $controller;
private FakeRenderer $renderer;
protected function setUp(): void
{
parent::setUp();
$this->renderer = new FakeRenderer();
Services::injectMock('renderer', $this->renderer);
$result = $this->createMock(BaseResult::class);
$result->method('getResultArray')->willReturn([
[
'id' => 5,
'expense_receipt' => 'uploads/path/expense_receipt.pdf',
'receipt_path' => null,
],
[
'id' => 6,
'expense_receipt' => null,
'receipt_path' => 'uploads/receipt_path.pdf',
],
]);
$builder = $this->createMock(BaseBuilder::class);
$builder->method('select')->willReturnSelf();
$builder->method('join')->willReturnSelf();
$builder->method('where')->willReturnSelf();
$builder->method('orderBy')->willReturnSelf();
$builder->method('get')->willReturn($result);
$db = $this->createMock(BaseConnection::class);
$db->method('table')->willReturn($builder);
$this->controller = new TestableReimbursementController();
$this->controller->setDb($db);
}
public function testUnderProcessingBuildsViewWithExpenses()
{
$result = $this->controller->underProcessing();
$this->assertSame('fake-rendered:reimbursements/under_processing', $result);
$rendered = $this->renderer->getLastRender();
$this->assertSame('reimbursements/under_processing', $rendered['view']);
$expenses = $rendered['data']['expenses'];
$this->assertCount(2, $expenses);
$first = $expenses[0];
$this->assertSame(site_url('receipts/expense_receipt.pdf'), $first['expense_receipt_url']);
$this->assertSame(
'<a href="' . base_url('reimbursements/create?expense_id=5') . '" class="btn btn-sm btn-primary">Reimburse</a>',
$first['action']
);
$second = $expenses[1];
$this->assertSame(site_url('receipts/receipt_path.pdf'), $second['expense_receipt_url']);
$this->assertSame(
'<a href="' . base_url('reimbursements/create?expense_id=6') . '" class="btn btn-sm btn-primary">Reimburse</a>',
$second['action']
);
}
}
}