update tests
This commit is contained in:
@@ -13,6 +13,54 @@ class ApiAuthenticationAndAuthorizationTest extends TestCase
|
||||
use CreatesApiTestUsers;
|
||||
use RefreshDatabase;
|
||||
|
||||
|
||||
public function test_api_login_returns_token_and_user_object(): void
|
||||
{
|
||||
$user = $this->createApiUserWithRole('teacher', [
|
||||
'firstname' => 'Login',
|
||||
'lastname' => 'Tester',
|
||||
'email' => 'api.login@example.test',
|
||||
]);
|
||||
|
||||
$this->postJson('/api/v1/auth/login', [
|
||||
'email' => strtoupper($user->email),
|
||||
'password' => 'password',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('status', true)
|
||||
->assertJsonStructure([
|
||||
'token',
|
||||
'access_token',
|
||||
'token_type',
|
||||
'expires_in',
|
||||
'user' => ['id', 'name', 'firstname', 'lastname', 'email', 'roles'],
|
||||
])
|
||||
->assertJsonPath('user.email', 'api.login@example.test');
|
||||
}
|
||||
|
||||
public function test_session_login_also_returns_token_and_user_object_for_spa_clients(): void
|
||||
{
|
||||
$user = $this->createApiUserWithRole('teacher', [
|
||||
'firstname' => 'Session',
|
||||
'lastname' => 'Tester',
|
||||
'email' => 'session.login@example.test',
|
||||
]);
|
||||
|
||||
$this->postJson('/user/login', [
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('status', true)
|
||||
->assertJsonStructure([
|
||||
'token',
|
||||
'access_token',
|
||||
'user' => ['id', 'name', 'firstname', 'lastname', 'email', 'roles'],
|
||||
'next_url',
|
||||
])
|
||||
->assertJsonPath('user.email', 'session.login@example.test');
|
||||
}
|
||||
|
||||
public function test_auth_me_requires_authentication(): void
|
||||
{
|
||||
$this->getJson('/api/v1/auth/me')->assertUnauthorized();
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Administrator;
|
||||
|
||||
use App\Services\Administrator\AdministratorDashboardService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Mockery;
|
||||
use Tests\Concerns\CreatesApiTestUsers;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdministratorDashboardControllerTest extends TestCase
|
||||
{
|
||||
use CreatesApiTestUsers;
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Mockery::close();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_metrics_require_authentication(): void
|
||||
{
|
||||
$this->getJson('/api/v1/administrator/dashboard/metrics')
|
||||
->assertStatus(401);
|
||||
}
|
||||
|
||||
public function test_metrics_are_forbidden_for_non_admin_users(): void
|
||||
{
|
||||
$teacher = $this->createApiUserWithRole('teacher');
|
||||
|
||||
$this->actingAs($teacher, 'api')
|
||||
->getJson('/api/v1/administrator/dashboard/metrics')
|
||||
->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_metrics_return_payload_for_admin(): void
|
||||
{
|
||||
$admin = $this->createApiUserWithRole('administrator');
|
||||
|
||||
$mock = Mockery::mock(AdministratorDashboardService::class);
|
||||
$mock->shouldReceive('metrics')->once()->andReturn([
|
||||
'students' => 42,
|
||||
'teachers' => 5,
|
||||
]);
|
||||
$this->app->instance(AdministratorDashboardService::class, $mock);
|
||||
|
||||
$this->actingAs($admin, 'api')
|
||||
->getJson('/api/v1/administrator/dashboard/metrics')
|
||||
->assertOk()
|
||||
->assertJson(['students' => 42, 'teachers' => 5]);
|
||||
}
|
||||
|
||||
public function test_user_search_passes_query_to_service(): void
|
||||
{
|
||||
$admin = $this->createApiUserWithRole('administrator');
|
||||
|
||||
$mock = Mockery::mock(AdministratorDashboardService::class);
|
||||
$mock->shouldReceive('userSearch')
|
||||
->once()
|
||||
->with('smith')
|
||||
->andReturn(['results' => [['id' => 1, 'name' => 'Smith']]]);
|
||||
$this->app->instance(AdministratorDashboardService::class, $mock);
|
||||
|
||||
$this->actingAs($admin, 'api')
|
||||
->getJson('/api/v1/administrator/dashboard/user-search?query=smith')
|
||||
->assertOk()
|
||||
->assertJsonPath('results.0.name', 'Smith');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Administrator;
|
||||
|
||||
use App\Services\Administrator\Trophy\TrophyReportService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Mockery;
|
||||
use Tests\Concerns\CreatesApiTestUsers;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TrophyControllerTest extends TestCase
|
||||
{
|
||||
use CreatesApiTestUsers;
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Mockery::close();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_index_requires_authentication(): void
|
||||
{
|
||||
$this->getJson('/api/v1/administrator/trophy')
|
||||
->assertStatus(401);
|
||||
}
|
||||
|
||||
public function test_index_is_forbidden_for_non_admin_users(): void
|
||||
{
|
||||
$teacher = $this->createApiUserWithRole('teacher');
|
||||
|
||||
$this->actingAs($teacher, 'api')
|
||||
->getJson('/api/v1/administrator/trophy')
|
||||
->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_index_returns_projection_for_admin(): void
|
||||
{
|
||||
$admin = $this->createApiUserWithRole('administrator');
|
||||
|
||||
$mock = Mockery::mock(TrophyReportService::class);
|
||||
$mock->shouldReceive('projection')->once()->andReturn(['rows' => []]);
|
||||
$this->app->instance(TrophyReportService::class, $mock);
|
||||
|
||||
$this->actingAs($admin, 'api')
|
||||
->getJson('/api/v1/administrator/trophy')
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true)
|
||||
->assertJsonPath('data.rows', []);
|
||||
}
|
||||
|
||||
public function test_winners_returns_data_for_admin(): void
|
||||
{
|
||||
$admin = $this->createApiUserWithRole('administrator');
|
||||
|
||||
$mock = Mockery::mock(TrophyReportService::class);
|
||||
$mock->shouldReceive('winners')->once()->andReturn(['winners' => []]);
|
||||
$this->app->instance(TrophyReportService::class, $mock);
|
||||
|
||||
$this->actingAs($admin, 'api')
|
||||
->getJson('/api/v1/administrator/trophy/winners')
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
}
|
||||
|
||||
public function test_final_returns_data_for_admin(): void
|
||||
{
|
||||
$admin = $this->createApiUserWithRole('administrator');
|
||||
|
||||
$mock = Mockery::mock(TrophyReportService::class);
|
||||
$mock->shouldReceive('final')->once()->andReturn(['final' => []]);
|
||||
$this->app->instance(TrophyReportService::class, $mock);
|
||||
|
||||
$this->actingAs($admin, 'api')
|
||||
->getJson('/api/v1/administrator/trophy/final')
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
}
|
||||
|
||||
public function test_index_validates_percentile_range(): void
|
||||
{
|
||||
$admin = $this->createApiUserWithRole('administrator');
|
||||
|
||||
$this->actingAs($admin, 'api')
|
||||
->getJson('/api/v1/administrator/trophy?percentile=150')
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors(['percentile']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\BadgeScan;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\Concerns\CreatesApiTestUsers;
|
||||
use Tests\TestCase;
|
||||
|
||||
class BadgeScanControllerTest extends TestCase
|
||||
{
|
||||
use CreatesApiTestUsers;
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_scan_is_public_but_validates_badge_value(): void
|
||||
{
|
||||
$this->postJson('/api/v1/badge_scan/scan', [])
|
||||
->assertStatus(422)
|
||||
->assertJsonPath('status', false);
|
||||
}
|
||||
|
||||
public function test_scan_returns_404_for_unrecognized_badge(): void
|
||||
{
|
||||
$this->postJson('/api/v1/badge_scan/scan', ['badge_scan' => 'UNKNOWN-BADGE-123'])
|
||||
->assertNotFound()
|
||||
->assertJsonPath('status', false);
|
||||
}
|
||||
|
||||
public function test_logs_require_authentication(): void
|
||||
{
|
||||
$this->getJson('/api/v1/badge_scan/logs')
|
||||
->assertStatus(401);
|
||||
}
|
||||
|
||||
public function test_logs_are_forbidden_for_non_staff_roles(): void
|
||||
{
|
||||
$parent = $this->createApiUserWithRole('parent');
|
||||
|
||||
$this->actingAs($parent, 'api')
|
||||
->getJson('/api/v1/badge_scan/logs')
|
||||
->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_logs_return_data_for_staff(): void
|
||||
{
|
||||
$admin = $this->createApiUserWithRole('administrator');
|
||||
|
||||
$this->actingAs($admin, 'api')
|
||||
->getJson('/api/v1/badge_scan/logs')
|
||||
->assertOk()
|
||||
->assertJsonPath('status', true)
|
||||
->assertJsonStructure(['status', 'data' => ['logs', 'meta']]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Finance;
|
||||
|
||||
use App\Models\EventCharge;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\Concerns\CreatesApiTestUsers;
|
||||
use Tests\TestCase;
|
||||
|
||||
class EventChargeControllerTest extends TestCase
|
||||
{
|
||||
use CreatesApiTestUsers;
|
||||
use RefreshDatabase;
|
||||
|
||||
private function makeCharge(array $overrides = []): EventCharge
|
||||
{
|
||||
return EventCharge::query()->create(array_merge([
|
||||
'event_name' => 'Field Trip',
|
||||
'description' => 'Annual field trip charge',
|
||||
'amount' => 25.00,
|
||||
'charged' => 0,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
], $overrides));
|
||||
}
|
||||
|
||||
public function test_show_requires_authentication(): void
|
||||
{
|
||||
$charge = $this->makeCharge();
|
||||
|
||||
$this->getJson('/api/v1/finance/event-charges/' . $charge->id)
|
||||
->assertStatus(401);
|
||||
}
|
||||
|
||||
public function test_show_returns_event_charge(): void
|
||||
{
|
||||
$this->actingAsApiAdministrator();
|
||||
$charge = $this->makeCharge();
|
||||
|
||||
$this->getJson('/api/v1/finance/event-charges/' . $charge->id)
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true)
|
||||
->assertJsonPath('event_charge.id', $charge->id)
|
||||
->assertJsonPath('event_charge.event_name', 'Field Trip');
|
||||
}
|
||||
|
||||
public function test_show_returns_404_for_unknown_charge(): void
|
||||
{
|
||||
$this->actingAsApiAdministrator();
|
||||
|
||||
$this->getJson('/api/v1/finance/event-charges/999999')
|
||||
->assertNotFound();
|
||||
}
|
||||
|
||||
public function test_update_modifies_event_charge(): void
|
||||
{
|
||||
$this->actingAsApiAdministrator();
|
||||
$charge = $this->makeCharge();
|
||||
|
||||
$this->putJson('/api/v1/finance/event-charges/' . $charge->id, [
|
||||
'event_name' => 'Museum Visit',
|
||||
'amount' => 40.50,
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true)
|
||||
->assertJsonPath('event_charge.event_name', 'Museum Visit');
|
||||
|
||||
$this->assertDatabaseHas('event_charges', [
|
||||
'id' => $charge->id,
|
||||
'event_name' => 'Museum Visit',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_approve_marks_charge_as_charged(): void
|
||||
{
|
||||
$this->actingAsApiAdministrator();
|
||||
$charge = $this->makeCharge(['charged' => 0]);
|
||||
|
||||
$this->postJson('/api/v1/finance/event-charges/' . $charge->id . '/approve')
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$this->assertDatabaseHas('event_charges', [
|
||||
'id' => $charge->id,
|
||||
'charged' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_void_clears_charged_flag(): void
|
||||
{
|
||||
$this->actingAsApiAdministrator();
|
||||
$charge = $this->makeCharge(['charged' => 1]);
|
||||
|
||||
$this->postJson('/api/v1/finance/event-charges/' . $charge->id . '/void')
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$this->assertDatabaseHas('event_charges', [
|
||||
'id' => $charge->id,
|
||||
'charged' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_destroy_voids_the_charge(): void
|
||||
{
|
||||
$this->actingAsApiAdministrator();
|
||||
$charge = $this->makeCharge(['charged' => 1]);
|
||||
|
||||
$this->deleteJson('/api/v1/finance/event-charges/' . $charge->id)
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$this->assertDatabaseHas('event_charges', [
|
||||
'id' => $charge->id,
|
||||
'charged' => 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Parents;
|
||||
|
||||
use App\Models\AuthorizedUser;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\Concerns\CreatesApiTestUsers;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AuthorizedUsersControllerTest extends TestCase
|
||||
{
|
||||
use CreatesApiTestUsers;
|
||||
use RefreshDatabase;
|
||||
|
||||
private function makeAuthorizedUser(int $parentId, array $overrides = []): AuthorizedUser
|
||||
{
|
||||
return AuthorizedUser::query()->create(array_merge([
|
||||
'user_id' => $parentId,
|
||||
'firstname' => 'Aunt',
|
||||
'lastname' => 'Carer',
|
||||
'email' => 'carer-' . uniqid() . '@example.test',
|
||||
'phone_number' => '5551112222',
|
||||
'relation_to_user' => 'Aunt',
|
||||
'status' => 'pending',
|
||||
], $overrides));
|
||||
}
|
||||
|
||||
public function test_index_requires_authentication(): void
|
||||
{
|
||||
$this->getJson('/api/v1/parents/authorized-users')
|
||||
->assertStatus(401);
|
||||
}
|
||||
|
||||
public function test_index_is_forbidden_for_non_parent_users(): void
|
||||
{
|
||||
$teacher = $this->createApiUserWithRole('teacher');
|
||||
|
||||
$this->actingAs($teacher, 'api')
|
||||
->getJson('/api/v1/parents/authorized-users')
|
||||
->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_index_returns_only_the_parents_own_authorized_users(): void
|
||||
{
|
||||
$parent = $this->createApiUserWithRole('parent');
|
||||
$otherParent = $this->createApiUserWithRole('parent');
|
||||
|
||||
$own = $this->makeAuthorizedUser($parent->id);
|
||||
$this->makeAuthorizedUser($otherParent->id);
|
||||
|
||||
$response = $this->actingAs($parent, 'api')
|
||||
->getJson('/api/v1/parents/authorized-users')
|
||||
->assertOk()
|
||||
->assertJsonPath('status', true);
|
||||
|
||||
$this->assertCount(1, $response->json('data'));
|
||||
$this->assertSame($own->id, $response->json('data.0.id'));
|
||||
}
|
||||
|
||||
public function test_show_returns_404_for_unknown_authorized_user(): void
|
||||
{
|
||||
$parent = $this->createApiUserWithRole('parent');
|
||||
|
||||
$this->actingAs($parent, 'api')
|
||||
->getJson('/api/v1/parents/authorized-users/999999')
|
||||
->assertNotFound()
|
||||
->assertJsonPath('status', false);
|
||||
}
|
||||
|
||||
public function test_show_returns_404_for_another_parents_authorized_user(): void
|
||||
{
|
||||
$parent = $this->createApiUserWithRole('parent');
|
||||
$otherParent = $this->createApiUserWithRole('parent');
|
||||
$foreign = $this->makeAuthorizedUser($otherParent->id);
|
||||
|
||||
$this->actingAs($parent, 'api')
|
||||
->getJson('/api/v1/parents/authorized-users/' . $foreign->id)
|
||||
->assertNotFound();
|
||||
}
|
||||
|
||||
public function test_destroy_deletes_the_parents_authorized_user(): void
|
||||
{
|
||||
$parent = $this->createApiUserWithRole('parent');
|
||||
$record = $this->makeAuthorizedUser($parent->id);
|
||||
|
||||
$this->actingAs($parent, 'api')
|
||||
->deleteJson('/api/v1/parents/authorized-users/' . $record->id)
|
||||
->assertOk()
|
||||
->assertJsonPath('status', true);
|
||||
|
||||
$this->assertDatabaseMissing('authorized_users', ['id' => $record->id]);
|
||||
}
|
||||
|
||||
public function test_parent_cannot_delete_another_parents_authorized_user(): void
|
||||
{
|
||||
$parent = $this->createApiUserWithRole('parent');
|
||||
$otherParent = $this->createApiUserWithRole('parent');
|
||||
$foreign = $this->makeAuthorizedUser($otherParent->id);
|
||||
|
||||
$this->actingAs($parent, 'api')
|
||||
->deleteJson('/api/v1/parents/authorized-users/' . $foreign->id)
|
||||
->assertNotFound();
|
||||
|
||||
$this->assertDatabaseHas('authorized_users', ['id' => $foreign->id]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Public;
|
||||
|
||||
use App\Models\Competition;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PublicWinnersControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_competition_index_is_public_and_returns_published_competitions(): void
|
||||
{
|
||||
Competition::query()->create([
|
||||
'title' => 'Published Quiz Bowl',
|
||||
'school_year' => '2025-2026',
|
||||
'is_published' => 1,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
Competition::query()->create([
|
||||
'title' => 'Hidden Draft',
|
||||
'school_year' => '2025-2026',
|
||||
'is_published' => 0,
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/winners/competitions');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('status', true)
|
||||
->assertJsonStructure(['status', 'data' => ['competitions']]);
|
||||
|
||||
$titles = array_column($response->json('data.competitions'), 'title');
|
||||
$this->assertContains('Published Quiz Bowl', $titles);
|
||||
$this->assertNotContains('Hidden Draft', $titles);
|
||||
}
|
||||
|
||||
public function test_competition_index_returns_empty_collection_when_none_published(): void
|
||||
{
|
||||
$this->getJson('/api/winners/competitions')
|
||||
->assertOk()
|
||||
->assertJsonPath('status', true)
|
||||
->assertJsonPath('data.competitions', []);
|
||||
}
|
||||
|
||||
public function test_competition_show_returns_payload_for_published_competition(): void
|
||||
{
|
||||
$competition = Competition::query()->create([
|
||||
'title' => 'Spelling Bee',
|
||||
'school_year' => '2025-2026',
|
||||
'is_published' => 1,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
$this->getJson('/api/winners/competitions/' . $competition->id)
|
||||
->assertOk()
|
||||
->assertJsonPath('status', true)
|
||||
->assertJsonPath('data.competition.id', $competition->id)
|
||||
->assertJsonStructure(['data' => ['competition', 'winners_by_class', 'section_map', 'question_counts']]);
|
||||
}
|
||||
|
||||
public function test_competition_show_returns_404_for_unknown_competition(): void
|
||||
{
|
||||
$this->getJson('/api/winners/competitions/999999')
|
||||
->assertNotFound()
|
||||
->assertJsonPath('status', false);
|
||||
}
|
||||
|
||||
public function test_competition_show_returns_404_for_unpublished_competition(): void
|
||||
{
|
||||
$competition = Competition::query()->create([
|
||||
'title' => 'Unpublished',
|
||||
'school_year' => '2025-2026',
|
||||
'is_published' => 0,
|
||||
]);
|
||||
|
||||
$this->getJson('/api/winners/competitions/' . $competition->id)
|
||||
->assertNotFound()
|
||||
->assertJsonPath('status', false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\Concerns\CreatesApiTestUsers;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ScannerControllerTest extends TestCase
|
||||
{
|
||||
use CreatesApiTestUsers;
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->seedApiTestConfig();
|
||||
}
|
||||
|
||||
public function test_process_requires_a_badge_code(): void
|
||||
{
|
||||
$this->postJson('/api/v1/scanner/process', ['badgeCode' => ''])
|
||||
->assertStatus(400)
|
||||
->assertJsonPath('status', false)
|
||||
->assertJsonPath('ok', false);
|
||||
}
|
||||
|
||||
public function test_process_returns_404_for_unknown_badge(): void
|
||||
{
|
||||
$this->postJson('/api/v1/scanner/process', ['badgeCode' => 'NO-SUCH-BADGE'])
|
||||
->assertNotFound()
|
||||
->assertJsonPath('status', false)
|
||||
->assertJsonPath('ok', false);
|
||||
}
|
||||
|
||||
public function test_process_accepts_alternative_badge_field_names(): void
|
||||
{
|
||||
$this->postJson('/api/v1/scanner/process', ['barcode' => 'STILL-UNKNOWN'])
|
||||
->assertNotFound()
|
||||
->assertJsonPath('status', false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\System;
|
||||
|
||||
use App\Models\Stats;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class StatsControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_stats_require_authentication(): void
|
||||
{
|
||||
$this->getJson('/api/v1/stats')
|
||||
->assertStatus(401);
|
||||
}
|
||||
|
||||
public function test_stats_index_returns_rows_for_authenticated_user(): void
|
||||
{
|
||||
Stats::query()->create([
|
||||
'students' => 12,
|
||||
'teachers' => 3,
|
||||
'admins' => 2,
|
||||
'users' => 17,
|
||||
]);
|
||||
|
||||
Sanctum::actingAs(User::factory()->create());
|
||||
|
||||
$response = $this->getJson('/api/v1/stats');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('status', true)
|
||||
->assertJsonStructure(['status', 'message', 'data']);
|
||||
|
||||
$this->assertSame(12, $response->json('data.0.students'));
|
||||
}
|
||||
|
||||
public function test_stats_index_returns_empty_array_when_no_stats(): void
|
||||
{
|
||||
Sanctum::actingAs(User::factory()->create());
|
||||
|
||||
$this->getJson('/api/v1/stats')
|
||||
->assertOk()
|
||||
->assertJsonPath('status', true)
|
||||
->assertJsonPath('data', []);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Utilities;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ProofreadControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_proofread_requires_authentication(): void
|
||||
{
|
||||
$this->postJson('/api/proofread', ['text' => 'hello'])
|
||||
->assertStatus(401);
|
||||
}
|
||||
|
||||
public function test_proofread_rejects_empty_text(): void
|
||||
{
|
||||
Sanctum::actingAs(User::factory()->create());
|
||||
|
||||
$this->postJson('/api/proofread', ['text' => ''])
|
||||
->assertStatus(422)
|
||||
->assertJsonPath('ok', false);
|
||||
}
|
||||
|
||||
public function test_proofread_rejects_text_that_is_too_long(): void
|
||||
{
|
||||
Sanctum::actingAs(User::factory()->create());
|
||||
|
||||
$this->postJson('/api/proofread', ['text' => str_repeat('a', 20001)])
|
||||
->assertStatus(422)
|
||||
->assertJsonPath('ok', false);
|
||||
}
|
||||
|
||||
public function test_proofread_proxies_languagetool_and_returns_result(): void
|
||||
{
|
||||
Http::fake([
|
||||
'api.languagetool.org/*' => Http::response(['matches' => []], 200),
|
||||
]);
|
||||
|
||||
Sanctum::actingAs(User::factory()->create());
|
||||
|
||||
$this->postJson('/api/proofread', ['text' => 'This are a test.'])
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true)
|
||||
->assertJsonStructure(['ok', 'result', 'csrfHash']);
|
||||
|
||||
Http::assertSent(fn ($request) => str_contains($request->url(), 'languagetool.org'));
|
||||
}
|
||||
|
||||
public function test_proofread_returns_502_when_upstream_fails(): void
|
||||
{
|
||||
Http::fake([
|
||||
'api.languagetool.org/*' => Http::response('error', 500),
|
||||
]);
|
||||
|
||||
Sanctum::actingAs(User::factory()->create());
|
||||
|
||||
$this->postJson('/api/proofread', ['text' => 'Some text to check.'])
|
||||
->assertStatus(502)
|
||||
->assertJsonPath('ok', false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Scenario A: New Family Registration to Active Enrollment.
|
||||
*
|
||||
* @see docs/alrahma_end_to_end_use_case_test_plan.md Scenario A
|
||||
*/
|
||||
class ScenarioAFamilyEnrollmentTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
|
||||
public function test_admin_onboards_family_through_active_enrollment(): void
|
||||
{
|
||||
Event::fake();
|
||||
$this->seedE2EConfiguration();
|
||||
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$classSectionId = $this->seedClassSection(701, '5A', 5);
|
||||
$teacher = $this->createApiUserWithRole('teacher');
|
||||
$this->seedTeacherClassAssignment($teacher->id, $classSectionId);
|
||||
|
||||
$parentId = $this->createParentAccountViaApi([
|
||||
'firstname' => 'Khadija',
|
||||
'lastname' => 'Saleh',
|
||||
'email' => 'khadija.saleh@example.test',
|
||||
]);
|
||||
$studentId = $this->createStudentViaApi($parentId, [
|
||||
'firstname' => 'Yusuf',
|
||||
'lastname' => 'Saleh',
|
||||
'name' => 'Khadija Saleh',
|
||||
'email' => 'khadija.saleh@example.test',
|
||||
]);
|
||||
|
||||
$this->assignStudentToClassViaApi($studentId, $classSectionId)
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$this->postEnrollmentStatuses([$studentId => 'enrolled'])->assertOk();
|
||||
|
||||
$this->assertDatabaseHas('enrollments', [
|
||||
'student_id' => $studentId,
|
||||
'parent_id' => $parentId,
|
||||
'enrollment_status' => 'enrolled',
|
||||
]);
|
||||
|
||||
// Teacher sees student in roster.
|
||||
$this->actingAs($teacher, 'api');
|
||||
$this->getJson('/api/v1/teachers/classes?class_section_id=' . $classSectionId)
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$studentIds = array_column($this->getJson('/api/v1/teachers/classes?class_section_id=' . $classSectionId)->json('students'), 'student_id');
|
||||
$this->assertContains($studentId, $studentIds);
|
||||
|
||||
// Parent sees own profile.
|
||||
$parent = User::query()->findOrFail($parentId);
|
||||
$this->actingAs($parent, 'api');
|
||||
$this->getJson('/api/v1/parents/profile')
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
// Admin can search the new user.
|
||||
$this->actingAs($admin, 'api');
|
||||
$this->getJson('/api/v1/administrator/dashboard/user-search?query=Saleh')
|
||||
->assertOk();
|
||||
}
|
||||
|
||||
public function test_parent_cannot_update_unrelated_student(): void
|
||||
{
|
||||
Event::fake();
|
||||
$this->seedE2EConfiguration();
|
||||
|
||||
$this->actingAsApiAdministrator();
|
||||
$parentA = $this->createApiUserWithRole('parent');
|
||||
$parentB = $this->createApiUserWithRole('parent');
|
||||
|
||||
$studentB = $this->createStudentViaApi($parentB->id, [
|
||||
'firstname' => 'Other',
|
||||
'lastname' => 'Child',
|
||||
]);
|
||||
|
||||
$this->actingAs($parentA, 'api');
|
||||
$this->patchJson('/api/v1/parents/students/' . $studentB, [
|
||||
'firstname' => 'Hacked',
|
||||
'lastname' => 'Child',
|
||||
'dob' => '2015-01-01',
|
||||
'gender' => 'Male',
|
||||
])->assertNotFound();
|
||||
|
||||
$this->assertDatabaseHas('students', [
|
||||
'id' => $studentB,
|
||||
'firstname' => 'Other',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Scenario B: Normal Sunday School Day.
|
||||
*
|
||||
* @see docs/alrahma_end_to_end_use_case_test_plan.md Scenario B
|
||||
*/
|
||||
class ScenarioBSundaySchoolDayTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
|
||||
public function test_teacher_submits_attendance_and_parent_views_report(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$teacher = $world['teacher'];
|
||||
$classSectionId = $world['class_section_id'];
|
||||
$studentId = $world['student_id'];
|
||||
$parent = $world['parent'];
|
||||
|
||||
$this->actingAs($teacher, 'api');
|
||||
|
||||
$form = $this->getJson('/api/v1/attendance/teacher/form?class_section_id=' . $classSectionId)
|
||||
->assertOk();
|
||||
|
||||
$date = (string) ($form->json('data.current_sunday') ?? '2025-10-05');
|
||||
$submit = $this->postJson('/api/v1/attendance/teacher/submit', [
|
||||
'class_section_id' => $classSectionId,
|
||||
'date' => $date,
|
||||
'attendance' => [
|
||||
['student_id' => $studentId, 'status' => 'present'],
|
||||
],
|
||||
'teachers' => [
|
||||
(string) $teacher->id => ['status' => 'present'],
|
||||
],
|
||||
]);
|
||||
|
||||
$submit->assertOk();
|
||||
$this->assertDatabaseHas('attendance_data', [
|
||||
'student_id' => $studentId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'status' => 'present',
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
]);
|
||||
|
||||
// Parent views attendance for their child.
|
||||
$this->actingAs($parent, 'api');
|
||||
$this->getJson('/api/v1/parents/attendance?school_year=' . self::E2E_SCHOOL_YEAR)
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
}
|
||||
|
||||
public function test_teacher_cannot_submit_attendance_for_unassigned_class(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$otherSectionId = $this->seedClassSection(999, '9Z', 9);
|
||||
$otherTeacher = $this->createApiUserWithRole('teacher');
|
||||
$this->seedTeacherClassAssignment($otherTeacher->id, $otherSectionId);
|
||||
|
||||
$this->actingAs($world['teacher'], 'api');
|
||||
$this->postJson('/api/v1/attendance/teacher/submit', [
|
||||
'class_section_id' => $otherSectionId,
|
||||
'date' => '2025-10-05',
|
||||
'attendance' => [
|
||||
['student_id' => $world['student_id'], 'status' => 'present'],
|
||||
],
|
||||
'teachers' => [
|
||||
(string) $world['teacher']->id => ['status' => 'present'],
|
||||
],
|
||||
])->assertStatus(422);
|
||||
|
||||
$this->assertDatabaseMissing('attendance_data', [
|
||||
'student_id' => $world['student_id'],
|
||||
'class_section_id' => $otherSectionId,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_admin_views_daily_attendance_summary(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
|
||||
DB::table('attendance_data')->insert([
|
||||
'class_id' => 3,
|
||||
'class_section_id' => $world['class_section_id'],
|
||||
'student_id' => $world['student_id'],
|
||||
'school_id' => '1',
|
||||
'date' => '2025-10-05',
|
||||
'status' => 'late',
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->actingAs($admin, 'api');
|
||||
$this->getJson('/api/v1/attendance/admin/daily?date=2025-10-05')
|
||||
->assertOk();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Scenario C: Academic Term Completion.
|
||||
*
|
||||
* @see docs/alrahma_end_to_end_use_case_test_plan.md Scenario C
|
||||
*/
|
||||
class ScenarioCAcademicTermTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
|
||||
public function test_teacher_enters_scores_admin_locks_grading_and_report_card_is_generated(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$teacher = $world['teacher'];
|
||||
$classSectionId = $world['class_section_id'];
|
||||
$studentId = $world['student_id'];
|
||||
|
||||
DB::table('homework')->insert([
|
||||
'student_id' => $studentId,
|
||||
'school_id' => 1,
|
||||
'class_section_id' => $classSectionId,
|
||||
'updated_by' => $teacher->id,
|
||||
'homework_index' => 1,
|
||||
'score' => 85,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('semester_scores')->insert([
|
||||
'student_id' => $studentId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'school_id' => 'E2E-SCORE-1',
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'midterm_exam_score' => '85',
|
||||
'participation_score' => '10',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->actingAs($teacher, 'api');
|
||||
|
||||
$this->getJson('/api/v1/scores/overview?class_section_id=' . $classSectionId . '&semester=' . self::E2E_SEMESTER . '&school_year=' . self::E2E_SCHOOL_YEAR)
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$this->postJson('/api/v1/scores/lock', [
|
||||
'class_section_id' => $classSectionId,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'missing_ok' => [],
|
||||
])->assertOk()->assertJsonPath('ok', true);
|
||||
|
||||
$this->assertDatabaseHas('grading_locks', [
|
||||
'class_section_id' => $classSectionId,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'is_locked' => 1,
|
||||
]);
|
||||
|
||||
$this->getJson('/api/v1/reports/report-cards/students/' . $studentId . '?school_year=' . self::E2E_SCHOOL_YEAR . '&semester=' . self::E2E_SEMESTER)
|
||||
->assertOk();
|
||||
}
|
||||
|
||||
public function test_teacher_invalid_homework_score_is_rejected(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
|
||||
$this->actingAs($world['teacher'], 'api');
|
||||
$response = $this->postJson('/api/v1/scores/homework', [
|
||||
'class_section_id' => $world['class_section_id'],
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'scores' => [
|
||||
$world['student_id'] => [
|
||||
1 => 999,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertGreaterThanOrEqual(400, $response->getStatusCode());
|
||||
$this->assertNotSame(true, $response->json('ok'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Scenario D: Family Billing Lifecycle.
|
||||
*
|
||||
* @see docs/alrahma_end_to_end_use_case_test_plan.md Scenario D
|
||||
*/
|
||||
class ScenarioDFamilyBillingTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
|
||||
public function test_admin_runs_invoice_discount_payment_and_reporting_workflow(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$parentId = $world['parent_id'];
|
||||
$studentId = $world['student_id'];
|
||||
|
||||
DB::table('enrollments')->insert([
|
||||
'parent_id' => $parentId,
|
||||
'student_id' => $studentId,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'enrollment_status' => 'payment pending',
|
||||
'enrollment_date' => '2025-09-01',
|
||||
]);
|
||||
|
||||
$invoiceId = $this->seedInvoiceForParent($parentId);
|
||||
$voucherId = $this->seedDiscountVoucher();
|
||||
|
||||
$this->actingAs($admin, 'api');
|
||||
$this->postJson('/api/v1/discounts/apply', [
|
||||
'voucher_id' => $voucherId,
|
||||
'parent_ids' => [$parentId],
|
||||
'allow_additional' => true,
|
||||
])->assertOk()->assertJsonPath('ok', true);
|
||||
|
||||
$chargeResponse = $this->postJson('/api/v1/finance/event-charges', [
|
||||
'event_name' => 'Science Fair',
|
||||
'description' => 'Materials fee',
|
||||
'amount' => 15.00,
|
||||
'parent_id' => $parentId,
|
||||
'student_id' => $studentId,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
]);
|
||||
$chargeResponse->assertOk()->assertJsonPath('ok', true);
|
||||
$chargeId = (int) DB::table('event_charges')
|
||||
->where('parent_id', $parentId)
|
||||
->where('student_id', $studentId)
|
||||
->where('event_name', 'Science Fair')
|
||||
->orderByDesc('id')
|
||||
->value('id');
|
||||
$this->assertGreaterThan(0, $chargeId);
|
||||
|
||||
$this->postJson('/api/v1/finance/event-charges/' . $chargeId . '/approve')
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$this->postJson('/api/v1/finance/payments', [
|
||||
'parent_id' => $parentId,
|
||||
'invoice_id' => $invoiceId,
|
||||
'total_amount' => 100,
|
||||
'number_of_installments' => 1,
|
||||
'payment_date' => '2025-10-01',
|
||||
'payment_method' => 'cash',
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
])->assertStatus(201)->assertJsonPath('ok', true);
|
||||
|
||||
$this->assertDatabaseHas('payments', [
|
||||
'parent_id' => $parentId,
|
||||
'invoice_id' => $invoiceId,
|
||||
]);
|
||||
|
||||
$this->getJson('/api/v1/finance/payments/parent/' . $parentId . '?school_year=' . self::E2E_SCHOOL_YEAR)
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$this->getJson('/api/v1/finance/unpaid-parents')
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Scenario E: Finance Edge Lifecycle (carryforward + installments + follow-ups).
|
||||
*
|
||||
* @see docs/alrahma_end_to_end_use_case_test_plan.md Scenario E
|
||||
*/
|
||||
class ScenarioEFinanceEdgeTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
|
||||
public function test_admin_carries_forward_balance_and_manages_installments(): void
|
||||
{
|
||||
$this->seedE2EConfiguration();
|
||||
$this->seedClosedSchoolYear();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
|
||||
$parent = $this->createApiUserWithRole('parent');
|
||||
$priorInvoiceId = $this->seedPriorYearUnpaidInvoice($parent->id);
|
||||
|
||||
$preview = $this->getJson('/api/v1/finance/carryforwards/preview?' . http_build_query([
|
||||
'from_school_year' => self::E2E_PREV_SCHOOL_YEAR,
|
||||
'to_school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'parent_id' => $parent->id,
|
||||
]));
|
||||
$preview->assertOk()->assertJsonPath('ok', true);
|
||||
$this->assertNotEmpty($preview->json('preview.rows'));
|
||||
|
||||
$draft = $this->postJson('/api/v1/finance/carryforwards/draft', [
|
||||
'from_school_year' => self::E2E_PREV_SCHOOL_YEAR,
|
||||
'to_school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'parent_id' => $parent->id,
|
||||
'reason' => 'E2E carryforward',
|
||||
]);
|
||||
$draft->assertOk()->assertJsonPath('ok', true);
|
||||
$this->assertNotEmpty($draft->json('result.created'));
|
||||
|
||||
$carryforwardId = (int) $draft->json('result.created.0.id');
|
||||
$this->postJson('/api/v1/finance/carryforwards/' . $carryforwardId . '/approve')
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$posted = $this->postJson('/api/v1/finance/carryforwards/' . $carryforwardId . '/post')
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
$newInvoiceId = (int) ($posted->json('carryforward.posted_invoice_id') ?? 0);
|
||||
$this->assertGreaterThan(0, $newInvoiceId);
|
||||
|
||||
$plan = $this->postJson('/api/v1/finance/invoices/' . $newInvoiceId . '/installment-plans', [
|
||||
'number_of_installments' => 2,
|
||||
'first_due_date' => '2025-09-15',
|
||||
'interval_months' => 1,
|
||||
])->assertCreated()->assertJsonPath('ok', true);
|
||||
|
||||
$planId = (int) $plan->json('plan.id');
|
||||
$this->postJson('/api/v1/finance/installment-plans/' . $planId . '/activate')
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$paymentId = DB::table('payments')->insertGetId([
|
||||
'parent_id' => $parent->id,
|
||||
'invoice_id' => $newInvoiceId,
|
||||
'total_amount' => 75.00,
|
||||
'paid_amount' => 75.00,
|
||||
'balance' => 0,
|
||||
'number_of_installments' => 1,
|
||||
'payment_method' => 'cash',
|
||||
'payment_date' => '2025-09-20',
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'status' => 'Paid',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->postJson('/api/v1/finance/payments/' . $paymentId . '/allocate-installments', [])
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$this->getJson('/api/v1/finance/installments/overdue')
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$this->postJson('/api/v1/finance/parent-payment-followups/' . $parent->id . '/note', [
|
||||
'invoice_id' => $newInvoiceId,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'note' => 'Called parent about prior-year balance.',
|
||||
'contact_method' => 'phone',
|
||||
])->assertCreated()->assertJsonPath('ok', true);
|
||||
|
||||
$this->postJson('/api/v1/finance/parent-payment-followups/' . $parent->id . '/promise-to-pay', [
|
||||
'invoice_id' => $newInvoiceId,
|
||||
'promise_to_pay_date' => '2025-10-15',
|
||||
'promise_to_pay_amount' => 50,
|
||||
])->assertCreated()->assertJsonPath('ok', true);
|
||||
|
||||
$this->postJson('/api/v1/finance/parent-payment-followups/' . $parent->id . '/resolve', [
|
||||
'invoice_id' => $newInvoiceId,
|
||||
'note' => 'Balance plan agreed.',
|
||||
])->assertCreated()->assertJsonPath('ok', true);
|
||||
|
||||
$this->assertDatabaseHas('finance_balance_carryforwards', [
|
||||
'id' => $carryforwardId,
|
||||
'parent_id' => $parent->id,
|
||||
'status' => 'posted_to_new_year',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('invoices', [
|
||||
'id' => $priorInvoiceId,
|
||||
'parent_id' => $parent->id,
|
||||
'school_year' => self::E2E_PREV_SCHOOL_YEAR,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Scenario F: Teacher Submission Follow-Up.
|
||||
*
|
||||
* @see docs/alrahma_end_to_end_use_case_test_plan.md Scenario F
|
||||
*/
|
||||
class ScenarioFTeacherSubmissionTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
|
||||
public function test_admin_detects_missing_submission_and_notifies_teacher(): void
|
||||
{
|
||||
Mail::fake();
|
||||
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$teacher = $world['teacher'];
|
||||
$classSectionId = $world['class_section_id'];
|
||||
|
||||
$this->actingAs($admin, 'api');
|
||||
$report = $this->getJson('/api/v1/administrator/teacher-submissions');
|
||||
$report->assertOk();
|
||||
$this->assertNotEmpty($report->json('rows'));
|
||||
|
||||
$row = collect($report->json('rows'))
|
||||
->firstWhere('class_section_id', $classSectionId);
|
||||
$this->assertNotNull($row);
|
||||
$this->assertNotEmpty($row['missing_items'] ?? []);
|
||||
|
||||
$notify = $this->postJson('/api/v1/administrator/teacher-submissions/notify', [
|
||||
'notify' => [
|
||||
$classSectionId => [
|
||||
$teacher->id => 1,
|
||||
],
|
||||
],
|
||||
'missing_items' => [
|
||||
$classSectionId => [
|
||||
$teacher->id => base64_encode(json_encode($row['missing_items'])),
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$notify->assertOk()->assertJsonPath('sent', 1);
|
||||
|
||||
$this->assertDatabaseHas('teacher_submission_notification_history', [
|
||||
'teacher_id' => $teacher->id,
|
||||
'class_section_id' => $classSectionId,
|
||||
'admin_id' => $admin->id,
|
||||
'notification_category' => 'teacher_submissions',
|
||||
]);
|
||||
|
||||
// Teacher submits missing homework after reminder.
|
||||
$this->actingAs($teacher, 'api');
|
||||
$this->postJson('/api/v1/scores/homework', [
|
||||
'class_section_id' => $classSectionId,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'scores' => [
|
||||
$world['student_id'] => [
|
||||
1 => 88,
|
||||
],
|
||||
],
|
||||
])->assertOk()->assertJsonPath('ok', true);
|
||||
|
||||
$this->assertDatabaseHas('homework', [
|
||||
'student_id' => $world['student_id'],
|
||||
'class_section_id' => $classSectionId,
|
||||
'homework_index' => 1,
|
||||
'score' => 88,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_duplicate_notify_requires_valid_payload(): void
|
||||
{
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
|
||||
$this->actingAs($admin, 'api');
|
||||
$this->postJson('/api/v1/administrator/teacher-submissions/notify', [
|
||||
'missing_items' => [],
|
||||
])->assertStatus(422)
|
||||
->assertJsonValidationErrors(['notify']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Scenario G: Print and Badge Workflow.
|
||||
*
|
||||
* @see docs/alrahma_end_to_end_use_case_test_plan.md Scenario G
|
||||
*/
|
||||
class ScenarioGPrintBadgeTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
|
||||
public function test_print_request_badge_log_and_scan_workflow(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$teacher = $world['teacher'];
|
||||
$classSectionId = $world['class_section_id'];
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
|
||||
$this->actingAs($teacher, 'api');
|
||||
$create = $this->postJson('/api/v1/print-requests/hand-copy', [
|
||||
'class_id' => $classSectionId,
|
||||
'num_copies' => 2,
|
||||
'required_by' => '2026-09-13 11:00:00',
|
||||
'pickup_method' => 'Self Pickup',
|
||||
]);
|
||||
$create->assertCreated();
|
||||
$requestId = (int) $create->json('data.print_request.id');
|
||||
|
||||
$this->actingAs($admin, 'api');
|
||||
foreach (['assigned', 'done', 'delivered'] as $status) {
|
||||
$this->patchJson('/api/v1/print-requests/' . $requestId . '/status', [
|
||||
'status' => $status,
|
||||
])->assertOk();
|
||||
}
|
||||
|
||||
$this->assertDatabaseHas('print_requests', [
|
||||
'id' => $requestId,
|
||||
'status' => 'delivered',
|
||||
]);
|
||||
|
||||
$badgeTag = 'E2E-BADGE-' . random_int(10000, 99999);
|
||||
DB::table('users')->where('id', $teacher->id)->update(['rfid_tag' => $badgeTag]);
|
||||
|
||||
$this->postJson('/api/v1/badges/log-print', [
|
||||
'user_ids' => [$teacher->id],
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'roles' => [(string) $teacher->id => 'Teacher'],
|
||||
'classes' => [(string) $teacher->id => '3A'],
|
||||
])->assertOk()->assertJsonPath('ok', true);
|
||||
|
||||
$this->postJson('/api/v1/badge_scan/scan', [
|
||||
'badge_scan' => $badgeTag,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
])->assertOk();
|
||||
|
||||
$this->assertDatabaseHas('scan_log', [
|
||||
'user_id' => $teacher->id,
|
||||
'card_id' => $badgeTag,
|
||||
]);
|
||||
|
||||
$this->actingAs($admin, 'api');
|
||||
$this->getJson('/api/v1/badge_scan/logs')
|
||||
->assertOk()
|
||||
->assertJsonPath('status', true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Scenario H: Security and Unauthorized Access.
|
||||
*
|
||||
* @see docs/alrahma_end_to_end_use_case_test_plan.md Scenario H
|
||||
*/
|
||||
class ScenarioHSecurityUnauthorizedAccessTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
|
||||
public function test_cross_role_and_anonymous_access_is_blocked(): void
|
||||
{
|
||||
$this->seedE2EConfiguration();
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$parentA = $this->createApiUserWithRole('parent');
|
||||
$teacher = $world['teacher'];
|
||||
$otherSectionId = $this->seedClassSection(902, '8B', 8);
|
||||
$otherTeacher = $this->createApiUserWithRole('teacher');
|
||||
$this->seedTeacherClassAssignment($otherTeacher->id, $otherSectionId);
|
||||
|
||||
// Parent cannot update another family's student.
|
||||
$this->actingAs($parentA, 'api');
|
||||
$this->patchJson('/api/v1/parents/students/' . $world['student_id'], [
|
||||
'firstname' => 'Blocked',
|
||||
'lastname' => 'Update',
|
||||
'dob' => '2014-01-01',
|
||||
'gender' => 'Male',
|
||||
])->assertNotFound();
|
||||
|
||||
// Teacher cannot submit attendance for a class they are not assigned to.
|
||||
$this->actingAs($teacher, 'api');
|
||||
$this->postJson('/api/v1/attendance/teacher/submit', [
|
||||
'class_section_id' => $otherSectionId,
|
||||
'date' => '2025-10-05',
|
||||
'attendance' => [
|
||||
['student_id' => $world['student_id'], 'status' => 'present'],
|
||||
],
|
||||
'teachers' => [
|
||||
(string) $teacher->id => ['status' => 'present'],
|
||||
],
|
||||
])->assertStatus(422);
|
||||
|
||||
// Teacher cannot access admin-only dashboard routes.
|
||||
$this->getJson('/api/v1/administrator/dashboard/metrics')->assertForbidden();
|
||||
$this->getJson('/api/v1/print-requests/admin')->assertForbidden();
|
||||
|
||||
// Suspended user cannot log in after valid credentials.
|
||||
$suspended = $this->createApiUserWithRole('parent', [
|
||||
'email' => 'suspended.user@example.test',
|
||||
'password' => Hash::make('secret123'),
|
||||
'is_suspended' => 1,
|
||||
]);
|
||||
|
||||
$this->postJson('/api/v1/auth/login', [
|
||||
'email' => $suspended->email,
|
||||
'password' => 'secret123',
|
||||
])->assertStatus(403);
|
||||
|
||||
// Anonymous requests are rejected before business logic.
|
||||
$this->actingAsGuest('api');
|
||||
$this->getJson('/api/v1/finance/invoices/management')->assertUnauthorized();
|
||||
$this->getJson('/api/v1/administrator/dashboard/metrics')->assertUnauthorized();
|
||||
$this->getJson('/api/v1/parents/profile')->assertUnauthorized();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use App\Models\Role;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Tests\Concerns\CreatesApiTestUsers;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* End-to-end "happy path" for onboarding a family:
|
||||
* 1. An administrator signs in.
|
||||
* 2. They create a parent account.
|
||||
* 3. They register a student under that parent.
|
||||
* 4. They enrol the student into a class section.
|
||||
* 5. They move the student's enrollment status to "enrolled".
|
||||
*
|
||||
* Each step uses the real HTTP API so the controllers, form requests,
|
||||
* services and persistence layers are all exercised together.
|
||||
*/
|
||||
class StudentEnrollmentWorkflowTest extends TestCase
|
||||
{
|
||||
use CreatesApiTestUsers;
|
||||
use RefreshDatabase;
|
||||
|
||||
private const SCHOOL_YEAR = '2025-2026';
|
||||
private const SEMESTER = 'Fall';
|
||||
|
||||
public function test_admin_creates_account_adds_student_and_enrolls_them(): void
|
||||
{
|
||||
Event::fake();
|
||||
|
||||
// 1. Administrator is authenticated (also seeds roles + active school year).
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$parentRoleId = (int) Role::query()->where('name', 'parent')->value('id');
|
||||
$this->assertGreaterThan(0, $parentRoleId);
|
||||
|
||||
// 2. Create a brand-new parent account.
|
||||
$createParent = $this->postJson('/api/v1/users', [
|
||||
'firstname' => 'Khadija',
|
||||
'lastname' => 'Saleh',
|
||||
'gender' => 'Female',
|
||||
'cellphone' => '5551234567',
|
||||
'email' => 'khadija.saleh@example.test',
|
||||
'address_street' => '42 Cedar Lane',
|
||||
'city' => 'Brooklyn',
|
||||
'state' => 'NY',
|
||||
'zip' => '11201',
|
||||
'accept_school_policy' => true,
|
||||
'role_id' => $parentRoleId,
|
||||
'password' => 'secret123',
|
||||
'semester' => self::SEMESTER,
|
||||
'school_year' => self::SCHOOL_YEAR,
|
||||
'user_type' => 'primary',
|
||||
'status' => 'Active',
|
||||
'is_verified' => true,
|
||||
]);
|
||||
|
||||
$createParent->assertStatus(201)->assertJsonPath('ok', true);
|
||||
$parentId = (int) $createParent->json('user_id');
|
||||
$this->assertGreaterThan(0, $parentId);
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'id' => $parentId,
|
||||
'email' => 'khadija.saleh@example.test',
|
||||
]);
|
||||
$this->assertDatabaseHas('user_roles', [
|
||||
'user_id' => $parentId,
|
||||
'role_id' => $parentRoleId,
|
||||
]);
|
||||
|
||||
// 3. Register the parent's first student (triggers the first-child
|
||||
// emergency-contact branch in StudentController@store).
|
||||
$createStudent = $this->postJson('/api/v1/students', [
|
||||
'firstname' => 'Yusuf',
|
||||
'lastname' => 'Saleh',
|
||||
'dob' => '2015-05-10',
|
||||
'age' => 10,
|
||||
'gender' => 'Male',
|
||||
'is_active' => true,
|
||||
'registration_grade' => '5',
|
||||
'is_new' => true,
|
||||
'photo_consent' => true,
|
||||
'parent_id' => $parentId,
|
||||
'registration_date' => '2025-09-01',
|
||||
'tuition_paid' => false,
|
||||
'semester' => self::SEMESTER,
|
||||
'year_of_registration' => 2025,
|
||||
'school_year' => self::SCHOOL_YEAR,
|
||||
// first-child contact details:
|
||||
'name' => 'Khadija Saleh',
|
||||
'cellphone' => '5551234567',
|
||||
'email' => 'khadija.saleh@example.test',
|
||||
'relation' => 'Mother',
|
||||
]);
|
||||
|
||||
$createStudent->assertStatus(201)->assertJsonPath('ok', true);
|
||||
$studentId = (int) $createStudent->json('student.id');
|
||||
$this->assertGreaterThan(0, $studentId);
|
||||
|
||||
$this->assertDatabaseHas('students', [
|
||||
'id' => $studentId,
|
||||
'parent_id' => $parentId,
|
||||
'firstname' => 'Yusuf',
|
||||
'lastname' => 'Saleh',
|
||||
]);
|
||||
$this->assertDatabaseHas('emergency_contacts', [
|
||||
'parent_id' => $parentId,
|
||||
'emergency_contact_name' => 'Khadija Saleh',
|
||||
'relation' => 'Mother',
|
||||
]);
|
||||
|
||||
// 4. Enrol the student into a class section.
|
||||
$classSectionId = $this->seedClassSection();
|
||||
$enroll = $this->postJson('/api/v1/students/assign-class', [
|
||||
'student_id' => $studentId,
|
||||
'class_section_id' => [$classSectionId],
|
||||
]);
|
||||
|
||||
$enroll->assertOk()->assertJsonPath('ok', true);
|
||||
$this->assertDatabaseHas('student_class', [
|
||||
'student_id' => $studentId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'school_year' => self::SCHOOL_YEAR,
|
||||
]);
|
||||
|
||||
// The class roster endpoint reflects the assignment.
|
||||
$classes = $this->getJson('/api/v1/students/' . $studentId . '/classes');
|
||||
$classes->assertOk()->assertJsonPath('ok', true);
|
||||
$assignedIds = array_column($classes->json('classes'), 'class_section_id');
|
||||
$this->assertContains($classSectionId, $assignedIds);
|
||||
|
||||
// 5. Move the student to "enrolled" via the enrollment workflow.
|
||||
$statusResponse = $this->post(
|
||||
'/api/v1/administrator/enrollment-withdrawal/update-statuses?' .
|
||||
http_build_query(['enrollment_status' => [$studentId => 'enrolled']]),
|
||||
[]
|
||||
);
|
||||
|
||||
$statusResponse->assertOk();
|
||||
$this->assertDatabaseHas('enrollments', [
|
||||
'student_id' => $studentId,
|
||||
'parent_id' => $parentId,
|
||||
'school_year' => self::SCHOOL_YEAR,
|
||||
'semester' => self::SEMESTER,
|
||||
'enrollment_status' => 'enrolled',
|
||||
'admission_status' => 'accepted',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_admin_can_withdraw_an_enrolled_student(): void
|
||||
{
|
||||
Event::fake();
|
||||
|
||||
$this->actingAsApiAdministrator();
|
||||
$parentRoleId = (int) Role::query()->where('name', 'parent')->value('id');
|
||||
|
||||
$parentId = (int) $this->postJson('/api/v1/users', [
|
||||
'firstname' => 'Omar',
|
||||
'lastname' => 'Hadid',
|
||||
'gender' => 'Male',
|
||||
'cellphone' => '5559876543',
|
||||
'email' => 'omar.hadid@example.test',
|
||||
'address_street' => '7 Maple Ct',
|
||||
'city' => 'Queens',
|
||||
'state' => 'NY',
|
||||
'zip' => '11367',
|
||||
'accept_school_policy' => true,
|
||||
'role_id' => $parentRoleId,
|
||||
'password' => 'secret123',
|
||||
'semester' => self::SEMESTER,
|
||||
'school_year' => self::SCHOOL_YEAR,
|
||||
'user_type' => 'primary',
|
||||
'status' => 'Active',
|
||||
'is_verified' => true,
|
||||
])->json('user_id');
|
||||
|
||||
$studentId = (int) $this->postJson('/api/v1/students', [
|
||||
'firstname' => 'Layla',
|
||||
'lastname' => 'Hadid',
|
||||
'gender' => 'Female',
|
||||
'is_active' => true,
|
||||
'registration_grade' => '2',
|
||||
'parent_id' => $parentId,
|
||||
'semester' => self::SEMESTER,
|
||||
'school_year' => self::SCHOOL_YEAR,
|
||||
'name' => 'Omar Hadid',
|
||||
'cellphone' => '5559876543',
|
||||
'email' => 'omar.hadid@example.test',
|
||||
'relation' => 'Father',
|
||||
])->json('student.id');
|
||||
|
||||
// First enrol...
|
||||
$this->post(
|
||||
'/api/v1/administrator/enrollment-withdrawal/update-statuses?' .
|
||||
http_build_query(['enrollment_status' => [$studentId => 'enrolled']]),
|
||||
[]
|
||||
)->assertOk();
|
||||
|
||||
// ...then withdraw.
|
||||
$this->post(
|
||||
'/api/v1/administrator/enrollment-withdrawal/update-statuses?' .
|
||||
http_build_query(['enrollment_status' => [$studentId => 'withdrawn']]),
|
||||
[]
|
||||
)->assertOk();
|
||||
|
||||
$this->assertDatabaseHas('enrollments', [
|
||||
'student_id' => $studentId,
|
||||
'parent_id' => $parentId,
|
||||
'school_year' => self::SCHOOL_YEAR,
|
||||
'enrollment_status' => 'withdrawn',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_creating_a_user_requires_a_valid_role(): void
|
||||
{
|
||||
$this->actingAsApiAdministrator();
|
||||
|
||||
$this->postJson('/api/v1/users', [
|
||||
'firstname' => 'No',
|
||||
'lastname' => 'Role',
|
||||
'cellphone' => '5550001111',
|
||||
'email' => 'no.role@example.test',
|
||||
'address_street' => '1 Test St',
|
||||
'city' => 'Town',
|
||||
'state' => 'NY',
|
||||
'zip' => '10001',
|
||||
'accept_school_policy' => true,
|
||||
'role_id' => 999999,
|
||||
'password' => 'secret123',
|
||||
'semester' => self::SEMESTER,
|
||||
])->assertStatus(422)
|
||||
->assertJsonValidationErrors(['role_id']);
|
||||
}
|
||||
|
||||
public function test_student_registration_requires_first_child_contact_details(): void
|
||||
{
|
||||
$this->actingAsApiAdministrator();
|
||||
$parentRoleId = (int) Role::query()->where('name', 'parent')->value('id');
|
||||
|
||||
$parentId = (int) $this->postJson('/api/v1/users', [
|
||||
'firstname' => 'First',
|
||||
'lastname' => 'Parent',
|
||||
'gender' => 'Female',
|
||||
'cellphone' => '5552223333',
|
||||
'email' => 'first.parent@example.test',
|
||||
'address_street' => '9 Birch Rd',
|
||||
'city' => 'Bronx',
|
||||
'state' => 'NY',
|
||||
'zip' => '10451',
|
||||
'accept_school_policy' => true,
|
||||
'role_id' => $parentRoleId,
|
||||
'password' => 'secret123',
|
||||
'semester' => self::SEMESTER,
|
||||
'school_year' => self::SCHOOL_YEAR,
|
||||
])->json('user_id');
|
||||
|
||||
// First student for a parent must include name/cellphone/email.
|
||||
$this->postJson('/api/v1/students', [
|
||||
'firstname' => 'Solo',
|
||||
'lastname' => 'Child',
|
||||
'parent_id' => $parentId,
|
||||
'school_year' => self::SCHOOL_YEAR,
|
||||
])->assertStatus(422)
|
||||
->assertJsonValidationErrors(['name', 'cellphone', 'email']);
|
||||
}
|
||||
|
||||
private function seedClassSection(int $classSectionId = 701, int $classId = 1): int
|
||||
{
|
||||
DB::table('classSection')->insert([
|
||||
'class_id' => $classId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'class_section_name' => '5A',
|
||||
'semester' => self::SEMESTER,
|
||||
'school_year' => self::SCHOOL_YEAR,
|
||||
]);
|
||||
|
||||
return $classSectionId;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user