This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user