fix school year and related issues
Tests / PHPUnit (push) Failing after 34s

This commit is contained in:
root
2026-07-30 00:48:54 -04:00
parent f8f00c70c8
commit 81a72a4c59
27 changed files with 1512 additions and 52 deletions
@@ -0,0 +1,18 @@
<?php
namespace Tests\App\Config;
use CodeIgniter\Test\CIUnitTestCase;
final class SchoolYearRouteIntegrityTest extends CIUnitTestCase
{
public function testSchoolYearManagementRoutesAllowPrincipalRole(): void
{
$routes = file_get_contents(ROOTPATH . 'app/Config/Routes.php') ?: '';
$this->assertMatchesRegularExpression(
"/\\\$routes->group\\('administrator\\/school-years', \\['filter' => 'auth:[^']*\\bprincipal\\b[^']*'\\]/",
$routes
);
}
}
@@ -0,0 +1,108 @@
<?php
namespace Tests\App\Filters;
use App\Filters\SchoolYearWritableFilter;
use App\Models\SchoolYearModel;
use App\Services\SchoolYearContextService;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\Response;
use CodeIgniter\HTTP\URI;
use CodeIgniter\HTTP\UserAgent;
use CodeIgniter\Test\CIUnitTestCase;
use Config\App;
use Config\Services;
final class SchoolYearWritableFilterFakeModel extends SchoolYearModel
{
public function __construct(private readonly array $row)
{
}
public function active(): ?array
{
return $this->row;
}
}
final class SchoolYearWritableFilterTest extends CIUnitTestCase
{
protected function setUp(): void
{
parent::setUp();
Services::resetSingle('session');
Services::resetSingle('schoolYearContext');
session()->set('user_id', 123);
}
public function testPostAgainstClosedSelectedYearIsBlocked(): void
{
$this->useSchoolYearContext(['id' => 1, 'name' => '2025-2026', 'status' => 'closed']);
$request = $this->request('POST', 'https://example.test/administrator/grades/save');
$request->setHeader('Accept', 'application/json');
$result = (new SchoolYearWritableFilter())->before($request);
$this->assertInstanceOf(Response::class, $result);
$this->assertSame(409, $result->getStatusCode());
$this->assertStringContainsString('Read-only school year', $result->getBody());
}
public function testPostAgainstActiveSelectedYearIsAllowed(): void
{
$this->useSchoolYearContext(['id' => 2, 'name' => '2026-2027', 'status' => 'active']);
$request = $this->request('POST', 'https://example.test/administrator/grades/save');
$this->assertNull((new SchoolYearWritableFilter())->before($request));
}
public function testSchoolYearSelectionPostIsExempt(): void
{
$this->useSchoolYearContext(['id' => 1, 'name' => '2025-2026', 'status' => 'closed']);
$request = $this->request('POST', 'https://example.test/school-year/select');
$this->assertNull((new SchoolYearWritableFilter())->before($request));
}
public function testReadRequestsAreAllowedForClosedSelectedYear(): void
{
$this->useSchoolYearContext(['id' => 1, 'name' => '2025-2026', 'status' => 'closed']);
$request = $this->request('GET', 'https://example.test/administrator/grades');
$this->assertNull((new SchoolYearWritableFilter())->before($request));
}
public function testLegacyGetDeleteRouteAgainstClosedSelectedYearIsBlocked(): void
{
$this->useSchoolYearContext(['id' => 1, 'name' => '2025-2026', 'status' => 'closed']);
$request = $this->request('GET', 'https://example.test/administrator/teacher/delete/44');
$request->setHeader('Accept', 'application/json');
$result = (new SchoolYearWritableFilter())->before($request);
$this->assertInstanceOf(Response::class, $result);
$this->assertSame(409, $result->getStatusCode());
}
private function useSchoolYearContext(array $row): void
{
Services::injectMock(
'schoolYearContext',
new SchoolYearContextService(new SchoolYearWritableFilterFakeModel($row))
);
}
private function request(string $method, string $uri): IncomingRequest
{
$request = new IncomingRequest(config(App::class), new URI($uri), 'php://input', new UserAgent());
$request->setMethod($method);
return $request;
}
}
@@ -29,6 +29,8 @@ final class SchoolYearContextRequest extends MockIncomingRequest
final class SchoolYearContextFakeModel extends SchoolYearModel
{
private array $filters = [];
public function __construct(private readonly array $rows)
{
}
@@ -48,6 +50,35 @@ final class SchoolYearContextFakeModel extends SchoolYearModel
return null;
}
public function where($key, $value = null, ?bool $escape = null)
{
$this->filters[] = [$key, $value];
return $this;
}
public function orderBy($orderBy, string $direction = '', ?bool $escape = null)
{
return $this;
}
public function findAll(?int $limit = null, int $offset = 0)
{
$rows = array_values(array_filter($this->rows, function (array $row): bool {
foreach ($this->filters as [$key, $value]) {
if (($row[$key] ?? null) !== $value) {
return false;
}
}
return true;
}));
$this->filters = [];
return $limit !== null ? array_slice($rows, $offset, $limit) : array_slice($rows, $offset);
}
}
final class SchoolYearContextServiceTest extends CIUnitTestCase
@@ -116,4 +147,20 @@ final class SchoolYearContextServiceTest extends CIUnitTestCase
$this->assertNull(session()->get('semester'));
$this->assertNull(session()->get('active_school_year'));
}
public function testSingleClosingYearIsUsedAsReadonlyContextWhenNoActiveYearExists(): void
{
$service = new SchoolYearContextService(new SchoolYearContextFakeModel([
1 => ['id' => 1, 'name' => '2025-2026', 'status' => 'closing'],
2 => ['id' => 2, 'name' => '2026-2027', 'status' => 'draft'],
]));
$context = $service->resolve(new SchoolYearContextRequest());
$this->assertSame(1, $context->id());
$this->assertSame('2025-2026', $context->yearName());
$this->assertSame('closing', $context->status());
$this->assertTrue($context->isReadonly());
$this->assertFalse($context->isExplicitSelection());
}
}