236 lines
7.5 KiB
PHP
236 lines
7.5 KiB
PHP
<?php
|
|
|
|
namespace Tests\App\Services;
|
|
|
|
use App\Models\SchoolYearModel;
|
|
use App\Services\SchoolYearContextService;
|
|
use CodeIgniter\HTTP\URI;
|
|
use CodeIgniter\HTTP\UserAgent;
|
|
use CodeIgniter\Test\CIUnitTestCase;
|
|
use CodeIgniter\Test\Mock\MockIncomingRequest;
|
|
use Config\App;
|
|
|
|
final class SchoolYearContextRequest extends MockIncomingRequest
|
|
{
|
|
public function __construct(
|
|
private readonly array $gets = [],
|
|
private readonly array $posts = [],
|
|
string $method = 'GET'
|
|
)
|
|
{
|
|
parent::__construct(config(App::class), new URI('https://test.alrahmaisgl.org'), 'php://input', new UserAgent());
|
|
$this->setMethod($method);
|
|
}
|
|
|
|
public function getGet($key = null, $filter = null, $default = null)
|
|
{
|
|
if ($key === null) {
|
|
return $this->gets;
|
|
}
|
|
|
|
return $this->gets[$key] ?? $default;
|
|
}
|
|
|
|
public function getPost($index = null, $filter = null, $flags = null)
|
|
{
|
|
if ($index === null) {
|
|
return $this->posts;
|
|
}
|
|
|
|
return $this->posts[$index] ?? null;
|
|
}
|
|
}
|
|
|
|
final class SchoolYearContextFakeModel extends SchoolYearModel
|
|
{
|
|
private array $filters = [];
|
|
|
|
public function __construct(private readonly array $rows)
|
|
{
|
|
}
|
|
|
|
public function find($id = null)
|
|
{
|
|
return $this->rows[(int) $id] ?? null;
|
|
}
|
|
|
|
public function active(): ?array
|
|
{
|
|
foreach ($this->rows as $row) {
|
|
if (($row['status'] ?? '') === 'active') {
|
|
return $row;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
public function first()
|
|
{
|
|
return $this->findAll(1)[0] ?? null;
|
|
}
|
|
}
|
|
|
|
final class SchoolYearContextServiceTest extends CIUnitTestCase
|
|
{
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
if (method_exists(session(), 'isStarted') && ! session()->isStarted()) {
|
|
session()->start();
|
|
}
|
|
}
|
|
|
|
public function testClosedSessionSelectionIsRetainedAsExplicitReadonlyContext(): void
|
|
{
|
|
session()->set('selected_school_year_id', 1);
|
|
|
|
$service = new SchoolYearContextService(new SchoolYearContextFakeModel([
|
|
1 => ['id' => 1, 'name' => '2025-2026', 'status' => 'closed'],
|
|
2 => ['id' => 2, 'name' => '2026-2027', 'status' => 'active'],
|
|
]));
|
|
|
|
$context = $service->resolve(new SchoolYearContextRequest());
|
|
|
|
$this->assertSame(1, $context->id());
|
|
$this->assertSame('2025-2026', $context->yearName());
|
|
$this->assertTrue($context->isReadonly());
|
|
$this->assertTrue($context->isExplicitSelection());
|
|
$this->assertSame(1, session()->get('selected_school_year_id'));
|
|
}
|
|
|
|
public function testPostSchoolYearNameTakesPrecedenceOverActiveYearForWrites(): void
|
|
{
|
|
$service = new SchoolYearContextService(new SchoolYearContextFakeModel([
|
|
1 => ['id' => 1, 'name' => '2025-2026', 'status' => 'closed'],
|
|
2 => ['id' => 2, 'name' => '2026-2027', 'status' => 'active'],
|
|
]));
|
|
|
|
$context = $service->resolve(new SchoolYearContextRequest(
|
|
posts: ['school_year' => '2025-2026'],
|
|
method: 'POST'
|
|
));
|
|
|
|
$this->assertSame(1, $context->id());
|
|
$this->assertSame('2025-2026', $context->yearName());
|
|
$this->assertTrue($context->isReadonly());
|
|
$this->assertTrue($context->isExplicitSelection());
|
|
}
|
|
|
|
public function testPostSchoolYearIdTakesPrecedenceOverActiveYearForWrites(): void
|
|
{
|
|
$service = new SchoolYearContextService(new SchoolYearContextFakeModel([
|
|
1 => ['id' => 1, 'name' => '2025-2026', 'status' => 'closed'],
|
|
2 => ['id' => 2, 'name' => '2026-2027', 'status' => 'active'],
|
|
]));
|
|
|
|
$context = $service->resolve(new SchoolYearContextRequest(
|
|
posts: ['school_year_id' => '1'],
|
|
method: 'POST'
|
|
));
|
|
|
|
$this->assertSame(1, $context->id());
|
|
$this->assertSame('2025-2026', $context->yearName());
|
|
$this->assertTrue($context->isReadonly());
|
|
}
|
|
|
|
public function testCanResolveContextFromStoredSchoolYearName(): void
|
|
{
|
|
$service = new SchoolYearContextService(new SchoolYearContextFakeModel([
|
|
1 => ['id' => 1, 'name' => '2025-2026', 'status' => 'closed'],
|
|
2 => ['id' => 2, 'name' => '2026-2027', 'status' => 'active'],
|
|
]));
|
|
|
|
$context = $service->forYearName('2025-2026');
|
|
|
|
$this->assertSame(1, $context->id());
|
|
$this->assertSame('2025-2026', $context->yearName());
|
|
$this->assertTrue($context->isReadonly());
|
|
$this->assertTrue($context->isExplicitSelection());
|
|
}
|
|
|
|
public function testInvalidSessionSelectionFallsBackToActiveYearAndClearsSession(): void
|
|
{
|
|
session()->set('selected_school_year_id', 99);
|
|
|
|
$service = new SchoolYearContextService(new SchoolYearContextFakeModel([
|
|
2 => ['id' => 2, 'name' => '2026-2027', 'status' => 'active'],
|
|
]));
|
|
|
|
$context = $service->resolve(new SchoolYearContextRequest());
|
|
|
|
$this->assertSame(2, $context->id());
|
|
$this->assertSame('2026-2027', $context->yearName());
|
|
$this->assertNull(session()->get('selected_school_year_id'));
|
|
}
|
|
|
|
public function testSelectingActiveYearClearsHistoricalOverride(): void
|
|
{
|
|
session()->set('selected_school_year_id', 1);
|
|
session()->set('class_section_id', 44);
|
|
session()->set('semester', 'Fall');
|
|
session()->set('active_school_year', '2025-2026');
|
|
|
|
$service = new SchoolYearContextService(new SchoolYearContextFakeModel([
|
|
1 => ['id' => 1, 'name' => '2025-2026', 'status' => 'closed'],
|
|
2 => ['id' => 2, 'name' => '2026-2027', 'status' => 'Active'],
|
|
]));
|
|
|
|
$context = $service->select(2);
|
|
|
|
$this->assertSame(2, $context->id());
|
|
$this->assertTrue($context->isActive());
|
|
$this->assertFalse($context->isExplicitSelection());
|
|
$this->assertNull(session()->get('selected_school_year_id'));
|
|
$this->assertNull(session()->get('class_section_id'));
|
|
$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());
|
|
}
|
|
}
|