add api tests

This commit is contained in:
root
2026-06-08 02:08:04 -04:00
parent 8ca24ef03f
commit 79024235ef
28 changed files with 1446 additions and 130 deletions
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
@@ -0,0 +1,87 @@
<?php
namespace Tests\Feature\Api;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\Concerns\CreatesApiTestUsers;
use Tests\TestCase;
class ApiAttendanceTemplateFeatureTest extends TestCase
{
use CreatesApiTestUsers;
use RefreshDatabase;
public function test_attendance_comment_templates_can_be_managed_through_current_api(): void
{
$this->actingAsApiAdministrator();
$create = $this->postJson('/api/v1/attendance-comment-templates', [
'min_score' => 0,
'max_score' => 59,
'template_text' => 'Needs follow up with parent.',
'is_active' => true,
])
->assertCreated()
->assertJsonPath('status', 'success');
$id = (int) data_get($create->json(), 'data.id');
$this->assertGreaterThan(0, $id);
$this->getJson('/api/v1/attendance-comment-templates')
->assertOk()
->assertJsonPath('status', 'success')
->assertJsonFragment(['template_text' => 'Needs follow up with parent.']);
$this->getJson("/api/v1/attendance-comment-templates/$id")
->assertOk()
->assertJsonPath('data.id', $id);
$this->putJson("/api/v1/attendance-comment-templates/$id", [
'max_score' => 60,
'template_text' => 'Updated follow up text.',
])
->assertOk()
->assertJsonPath('status', 'success');
$this->deleteJson("/api/v1/attendance-comment-templates/$id")
->assertOk()
->assertJsonPath('status', 'success');
}
public function test_attendance_comment_template_validation_blocks_bad_ranges(): void
{
$this->actingAsApiAdministrator();
$this->postJson('/api/v1/attendance-comment-templates', [
'min_score' => 90,
'max_score' => 10,
'template_text' => '',
])
->assertStatus(422)
->assertJsonValidationErrors(['max_score', 'template_text']);
}
public function test_legacy_attendance_template_aliases_still_work(): void
{
$this->actingAsApiAdministrator();
$this->postJson('/api/v1/attendance-templates/save', [
'min_score' => 70,
'max_score' => 80,
'template_text' => 'Legacy alias text.',
'is_active' => 'on',
])
->assertOk()
->assertJsonPath('status', 'success');
$list = $this->getJson('/api/v1/attendance-templates')
->assertOk()
->assertJsonFragment(['template_text' => 'Legacy alias text.']);
$id = (int) collect($list->json('templates'))->firstWhere('template_text', 'Legacy alias text.')['id'];
$this->postJson('/api/v1/attendance-templates/delete', ['id' => $id])
->assertOk()
->assertJsonPath('status', 'success');
}
}
@@ -0,0 +1,126 @@
<?php
namespace Tests\Feature\Api;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Routing\Route as LaravelRoute;
use Illuminate\Support\Facades\Route;
use Tests\Concerns\CreatesApiTestUsers;
use Tests\TestCase;
class ApiAuthenticationAndAuthorizationTest extends TestCase
{
use CreatesApiTestUsers;
use RefreshDatabase;
public function test_auth_me_requires_authentication(): void
{
$this->getJson('/api/v1/auth/me')->assertUnauthorized();
}
public function test_auth_me_returns_the_current_authenticated_user(): void
{
$user = $this->createApiUserWithRole('teacher', [
'firstname' => 'Teacher',
'lastname' => 'Tester',
'email' => 'teacher.me@example.test',
]);
$this->actingAs($user, 'api')
->getJson('/api/v1/auth/me')
->assertOk()
->assertJsonPath('status', true)
->assertJsonPath('data.email', 'teacher.me@example.test');
}
public function test_admin_only_routes_reject_non_admin_users(): void
{
$parent = $this->createApiUserWithRole('parent');
$this->actingAs($parent, 'api');
foreach ($this->adminOnlyEndpoints() as [$method, $uri]) {
$response = $this->json($method, $uri);
$this->assertContains(
$response->getStatusCode(),
[401, 403],
"$method $uri should reject a non-admin user; got {$response->getStatusCode()}"
);
}
}
public function test_protected_api_routes_reject_anonymous_requests_before_business_logic(): void
{
$failures = [];
foreach ($this->sampleProtectedRoutes() as $route) {
$method = $this->primaryMethod($route);
$uri = '/' . $this->uriWithPlaceholders($route->uri());
$response = $this->json($method, $uri);
$status = $response->getStatusCode();
if (! in_array($status, [401, 403, 404, 405, 419, 422], true)) {
$failures[] = "$method $uri returned $status";
}
}
$this->assertSame([], $failures, "Anonymous requests reached unexpected responses:\n" . implode("\n", $failures));
}
/**
* @return list<array{0: string, 1: string}>
*/
private function adminOnlyEndpoints(): array
{
return [
['GET', '/api/v1/users'],
['POST', '/api/v1/users'],
['GET', '/api/v1/administrator/dashboard/metrics'],
['GET', '/api/v1/administrator/staff'],
['GET', '/api/v1/school-years'],
['GET', '/api/v1/administrator/role-permission/roles'],
];
}
/**
* @return list<LaravelRoute>
*/
private function sampleProtectedRoutes(): array
{
return collect(Route::getRoutes()->getRoutes())
->filter(fn (LaravelRoute $route): bool => str_starts_with($route->uri(), 'api/'))
->filter(function (LaravelRoute $route): bool {
return collect($route->gatherMiddleware())->contains(function (string $middleware): bool {
return str_contains($middleware, 'auth:api') || str_contains($middleware, 'jwt.auth');
});
})
->reject(function (LaravelRoute $route): bool {
$uri = $route->uri();
return str_contains($uri, 'files/')
|| str_contains($uri, 'receipts/')
|| str_contains($uri, 'reimbursements/')
|| str_contains($uri, 'attachments/');
})
->take(80)
->values()
->all();
}
private function primaryMethod(LaravelRoute $route): string
{
foreach ($route->methods() as $method) {
if (! in_array($method, ['HEAD', 'OPTIONS'], true)) {
return $method;
}
}
return 'GET';
}
private function uriWithPlaceholders(string $uri): string
{
return preg_replace('/\{[^}]+\}/', '1', $uri) ?? $uri;
}
}
@@ -0,0 +1,116 @@
<?php
namespace Tests\Feature\Api;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\Concerns\CreatesApiTestUsers;
use Tests\TestCase;
class ApiPreferencesFeatureTest extends TestCase
{
use CreatesApiTestUsers;
use RefreshDatabase;
public function test_authenticated_user_can_create_read_update_and_delete_preferences(): void
{
$user = $this->createApiUserWithRole('teacher');
$this->actingAs($user, 'api');
$this->postJson('/api/v1/preferences', [
'theme' => 'dark',
'language' => 'en',
'receive_email_notifications' => true,
'receive_sms_notifications' => false,
])
->assertStatus(201)
->assertJsonPath('status', true);
$this->getJson('/api/v1/preferences')
->assertOk()
->assertJsonPath('status', true)
->assertJsonPath('data.preferences.theme', 'dark');
$this->putJson("/api/v1/preferences/{$user->id}", [
'theme' => 'light',
'language' => 'es',
])
->assertOk()
->assertJsonPath('status', true);
$this->getJson("/api/v1/preferences/{$user->id}")
->assertOk()
->assertJsonPath('data.preferences.theme', 'light')
->assertJsonPath('data.preferences.language', 'es');
$this->deleteJson("/api/v1/preferences/{$user->id}")
->assertOk()
->assertJsonPath('status', true);
}
public function test_non_admin_cannot_list_or_manage_another_users_preferences(): void
{
$owner = $this->createApiUserWithRole('teacher');
$other = $this->createApiUserWithRole('parent');
$this->actingAs($owner, 'api');
$this->postJson('/api/v1/preferences', [
'theme' => 'dark',
'language' => 'en',
])->assertStatus(201);
$this->actingAs($other, 'api');
$this->postJson('/api/v1/preferences', [
'theme' => 'light',
'language' => 'en',
])->assertStatus(201);
$this->getJson('/api/v1/preferences/list')
->assertForbidden();
$this->getJson("/api/v1/preferences/{$owner->id}")
->assertForbidden();
$this->putJson("/api/v1/preferences/{$owner->id}", [
'theme' => 'light',
'language' => 'es',
])->assertForbidden();
$this->deleteJson("/api/v1/preferences/{$owner->id}")
->assertForbidden();
}
public function test_admin_can_list_and_delete_any_users_preferences(): void
{
$owner = $this->createApiUserWithRole('teacher');
$admin = $this->createApiUserWithRole('administrator');
$this->actingAs($owner, 'api');
$this->postJson('/api/v1/preferences', [
'theme' => 'dark',
'language' => 'en',
])->assertStatus(201);
$this->actingAs($admin, 'api');
$this->getJson('/api/v1/preferences/list')
->assertOk()
->assertJsonPath('status', true);
$this->deleteJson("/api/v1/preferences/{$owner->id}")
->assertOk()
->assertJsonPath('status', true);
}
public function test_preferences_validation_rejects_invalid_options(): void
{
$user = $this->createApiUserWithRole('teacher');
$this->actingAs($user, 'api');
$this->postJson('/api/v1/preferences', [
'theme' => 'neon-chaos',
'language' => 'klingon',
])
->assertStatus(422)
->assertJsonValidationErrors(['theme', 'language']);
}
}
@@ -0,0 +1,60 @@
<?php
namespace Tests\Feature\Api;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ApiPublicEndpointsTest extends TestCase
{
use RefreshDatabase;
public function test_health_endpoint_is_available(): void
{
$this->getJson('/api/v1/health')
->assertOk();
}
public function test_public_policy_endpoints_are_available(): void
{
foreach (['/api/v1/policies/school', '/api/v1/policies/picture'] as $uri) {
$response = $this->getJson($uri);
$this->assertLessThan(500, $response->getStatusCode(), "$uri returned a server error");
}
}
public function test_public_static_pages_are_available(): void
{
foreach (['privacy', 'terms', 'help'] as $page) {
$response = $this->getJson("/api/v1/pages/$page");
$this->assertLessThan(500, $response->getStatusCode(), "/api/v1/pages/$page returned a server error");
}
}
public function test_public_frontend_content_endpoints_do_not_server_error(): void
{
foreach (['/', 'facility', 'team', 'call-to-action', 'testimonial', 'not-found'] as $path) {
$uri = '/api/v1/frontend' . ($path === '/' ? '' : '/' . $path);
$response = $this->getJson($uri);
$this->assertLessThan(500, $response->getStatusCode(), "$uri returned a server error");
}
}
public function test_registration_captcha_endpoint_returns_captcha_payload(): void
{
$this->getJson('/api/v1/auth/register/captcha')
->assertOk()
->assertJsonPath('ok', true)
->assertJsonStructure(['ok', 'captcha']);
}
public function test_invalid_login_payload_is_rejected_cleanly(): void
{
$this->postJson('/api/v1/auth/login', [])
->assertStatus(400)
->assertJsonPath('status', false);
}
}
@@ -0,0 +1,108 @@
<?php
namespace Tests\Feature\Api;
use App\Models\Permission;
use App\Models\Role;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\Concerns\CreatesApiTestUsers;
use Tests\TestCase;
class ApiRolePermissionFeatureTest extends TestCase
{
use CreatesApiTestUsers;
use RefreshDatabase;
public function test_admin_can_manage_roles_permissions_and_assign_roles(): void
{
$this->actingAsApiAdministrator();
$roles = $this->seedApiRoles();
$target = User::factory()->create(['email' => 'assign-role-target@example.test']);
$roleResponse = $this->postJson('/api/v1/administrator/role-permission/roles', [
'name' => 'finance_manager',
'slug' => 'finance-manager',
'description' => 'Finance Manager',
'dashboard_route' => 'finance/dashboard',
'priority' => 20,
'is_active' => 1,
])
->assertCreated()
->assertJsonPath('status', true);
$roleId = (int) data_get($roleResponse->json(), 'data.role.id');
$this->assertGreaterThan(0, $roleId);
$this->getJson('/api/v1/administrator/role-permission/roles')
->assertOk()
->assertJsonFragment(['name' => 'finance_manager']);
$this->patchJson("/api/v1/administrator/role-permission/roles/$roleId", [
'description' => 'Updated Finance Manager',
'priority' => 21,
])
->assertOk()
->assertJsonPath('status', true);
$permissionResponse = $this->postJson('/api/v1/administrator/role-permission/permissions', [
'name' => 'finance.reports.view',
'description' => 'View finance reports',
])
->assertCreated()
->assertJsonPath('status', true);
$permissionId = (int) data_get($permissionResponse->json(), 'data.permission.id');
$this->assertGreaterThan(0, $permissionId);
$this->putJson("/api/v1/administrator/role-permission/roles/$roleId/permissions", [
'permissions' => [
(string) $permissionId => [
'read' => true,
'create' => false,
'update' => false,
'delete' => false,
'manage' => false,
],
],
])
->assertOk()
->assertJsonPath('status', true);
$this->postJson("/api/v1/administrator/role-permission/users/{$target->id}/roles", [
'role_ids' => [$roles['parent']->id, $roleId],
])
->assertOk()
->assertJsonPath('status', true);
$this->assertDatabaseHas('roles', ['id' => $roleId, 'priority' => 21]);
$this->assertDatabaseHas('permissions', ['id' => $permissionId, 'name' => 'finance.reports.view']);
$this->assertContains('finance_manager', $target->fresh()->roleNames());
$this->deleteJson("/api/v1/administrator/role-permission/permissions/$permissionId")
->assertOk()
->assertJsonPath('status', true);
$this->deleteJson("/api/v1/administrator/role-permission/roles/$roleId")
->assertOk()
->assertJsonPath('status', true);
}
public function test_role_permission_validation_rejects_bad_payloads(): void
{
$this->actingAsApiAdministrator();
$this->postJson('/api/v1/administrator/role-permission/roles', [
'name' => 'ab',
'dashboard_route' => 'bad route with spaces',
'priority' => -1,
'is_active' => 2,
])
->assertStatus(422)
->assertJsonValidationErrors(['name', 'dashboard_route', 'priority', 'is_active']);
$this->postJson('/api/v1/administrator/role-permission/permissions', [])
->assertStatus(422)
->assertJsonValidationErrors(['name']);
}
}
+109
View File
@@ -0,0 +1,109 @@
<?php
namespace Tests\Feature\Api;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Routing\Route as LaravelRoute;
use Illuminate\Support\Facades\Route;
use Tests\TestCase;
class ApiRouteContractTest extends TestCase
{
use RefreshDatabase;
public function test_every_api_route_points_to_an_existing_controller_method(): void
{
$missing = [];
foreach ($this->apiRoutes() as $route) {
$action = $route->getActionName();
if ($action === 'Closure' || ! str_contains($action, '@')) {
continue;
}
[$class, $method] = explode('@', $action, 2);
if (! class_exists($class) || ! method_exists($class, $method)) {
$missing[] = implode('|', $route->methods()) . ' ' . $route->uri() . ' -> ' . $action;
}
}
$this->assertSame([], $missing, "These API routes reference missing controllers or methods:\n" . implode("\n", $missing));
}
public function test_api_routes_do_not_duplicate_method_and_uri_pairs(): void
{
$seen = [];
$duplicates = [];
foreach ($this->apiRoutes() as $route) {
foreach ($route->methods() as $method) {
if ($method === 'HEAD') {
continue;
}
$key = $method . ' ' . $route->uri();
if (isset($seen[$key])) {
$duplicates[] = $key;
}
$seen[$key] = true;
}
}
$this->assertSame([], array_values(array_unique($duplicates)), "Duplicate API route method/URI pairs found. Ambiguous routing is not a feature, despite humanity's best efforts.");
}
public function test_mutating_api_routes_are_protected_unless_explicitly_public(): void
{
$publicAllowList = [
'api/login',
'api/register',
'api/v1/login',
'api/v1/register',
'api/v1/auth/login',
'api/v1/auth/register',
'api/v1/pages/contact',
'api/v1/contact',
'api/set_authorized_user_password/{authorizedUserId}',
'api/v1/badge_scan/scan',
'api/v1/scanner/process',
];
$unprotected = [];
foreach ($this->apiRoutes() as $route) {
$methods = array_diff($route->methods(), ['HEAD', 'OPTIONS']);
if (array_intersect($methods, ['POST', 'PUT', 'PATCH', 'DELETE']) === []) {
continue;
}
if (in_array($route->uri(), $publicAllowList, true)) {
continue;
}
$middleware = $route->gatherMiddleware();
$hasProtection = collect($middleware)->contains(function (string $middleware): bool {
return str_contains($middleware, 'auth')
|| str_contains($middleware, 'throttle')
|| str_contains($middleware, 'teacher.portal.auth')
|| str_contains($middleware, 'badge_scan');
});
if (! $hasProtection) {
$unprotected[] = implode('|', $methods) . ' ' . $route->uri();
}
}
$this->assertSame([], $unprotected, "Mutating API routes without an obvious auth/throttle middleware:\n" . implode("\n", $unprotected));
}
/**
* @return list<LaravelRoute>
*/
private function apiRoutes(): array
{
return array_values(array_filter(Route::getRoutes()->getRoutes(), function (LaravelRoute $route): bool {
return str_starts_with($route->uri(), 'api/');
}));
}
}
@@ -0,0 +1,88 @@
<?php
namespace Tests\Feature\Api;
use App\Models\Role;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Tests\Concerns\CreatesApiTestUsers;
use Tests\TestCase;
class ApiUserManagementFeatureTest extends TestCase
{
use CreatesApiTestUsers;
use RefreshDatabase;
public function test_admin_can_create_list_update_and_delete_a_user_through_api(): void
{
$this->actingAsApiAdministrator();
$roles = $this->seedApiRoles();
$createPayload = $this->validUserPayload((int) $roles['parent']->id, [
'email' => 'api-user-crud@example.test',
'firstname' => 'Original',
]);
$create = $this->postJson('/api/v1/users', $createPayload)
->assertCreated()
->assertJsonPath('ok', true);
$userId = (int) $create->json('user_id');
$this->assertGreaterThan(0, $userId);
$created = User::query()->findOrFail($userId);
$this->assertSame('api-user-crud@example.test', $created->email);
$this->assertTrue(Hash::check('password', (string) $created->password));
$this->assertSame(['parent'], $created->roleNames());
$this->getJson('/api/v1/users')
->assertOk()
->assertJsonPath('ok', true)
->assertJsonFragment(['email' => 'api-user-crud@example.test']);
$this->putJson("/api/v1/users/$userId", [
'firstname' => 'Updated',
'status' => 'Inactive',
])
->assertOk()
->assertJsonPath('ok', true);
$this->assertDatabaseHas('users', [
'id' => $userId,
'firstname' => 'Updated',
'status' => 'Inactive',
]);
$this->deleteJson("/api/v1/users/$userId")
->assertOk()
->assertJsonPath('ok', true);
}
public function test_user_creation_validates_required_payload_and_unique_email(): void
{
$this->actingAsApiAdministrator();
$roles = $this->seedApiRoles();
$this->postJson('/api/v1/users', [])
->assertStatus(422)
->assertJsonValidationErrors(['firstname', 'lastname', 'cellphone', 'email', 'address_street', 'city', 'state', 'zip', 'accept_school_policy', 'role_id', 'password', 'semester']);
User::factory()->create(['email' => 'already-used@example.test']);
$this->postJson('/api/v1/users', $this->validUserPayload((int) $roles['teacher']->id, [
'email' => 'already-used@example.test',
]))
->assertStatus(422)
->assertJsonValidationErrors(['email']);
}
public function test_user_creation_rejects_nonexistent_role_id(): void
{
$this->actingAsApiAdministrator();
$this->postJson('/api/v1/users', $this->validUserPayload(999999))
->assertStatus(422)
->assertJsonValidationErrors(['role_id']);
}
}
@@ -0,0 +1,249 @@
<?php
namespace Tests\Feature\Api\V1\PrintRequests;
use App\Models\PrintRequest;
use App\Models\Role;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class PrintRequestsWorkflowTest extends TestCase
{
use RefreshDatabase;
public function test_teacher_can_create_hand_copy_request_and_admin_can_advance_statuses(): void
{
$teacher = $this->createUserWithRole('teacher');
$admin = $this->createUserWithRole('administrator');
$classSectionId = $this->seedClassSection();
$this->actingAs($teacher, 'api');
$createResponse = $this->postJson('/api/v1/print-requests/hand-copy', [
'class_id' => $classSectionId,
'num_copies' => 3,
'required_by' => '2026-09-13 11:00:00',
'pickup_method' => 'Self Pickup',
]);
$createResponse
->assertCreated()
->assertJsonPath('status', true)
->assertJsonPath('data.print_request.status', 'not_assigned');
$requestId = (int) $createResponse->json('data.print_request.id');
$this->assertDatabaseHas('print_requests', [
'id' => $requestId,
'teacher_id' => $teacher->id,
'class_id' => $classSectionId,
'file_path' => '',
'num_copies' => 3,
'status' => 'not_assigned',
]);
$this->actingAs($admin, 'api');
foreach (['assigned', 'done', 'delivered'] as $status) {
$this->patchJson("/api/v1/print-requests/{$requestId}/status", [
'status' => $status,
])
->assertOk()
->assertJsonPath('status', true)
->assertJsonPath('data.print_request.status', $status);
}
$this->assertDatabaseHas('print_requests', [
'id' => $requestId,
'admin_id' => $admin->id,
'status' => 'delivered',
]);
}
public function test_admin_status_rejects_invalid_transition(): void
{
$teacher = $this->createUserWithRole('teacher');
$admin = $this->createUserWithRole('administrator');
$request = $this->seedPrintRequest($teacher, ['status' => 'delivered']);
$this->actingAs($admin, 'api');
$this->patchJson("/api/v1/print-requests/{$request->id}/status", [
'status' => 'assigned',
])
->assertUnprocessable()
->assertJsonPath('status', false);
$this->assertDatabaseHas('print_requests', [
'id' => $request->id,
'status' => 'delivered',
]);
}
public function test_teacher_routes_reject_non_teacher_roles_and_admin_routes_reject_teacher_roles(): void
{
$parent = $this->createUserWithRole('parent');
$teacher = $this->createUserWithRole('teacher');
$this->actingAs($parent, 'api');
$this->getJson('/api/v1/print-requests/teacher')
->assertForbidden();
$this->actingAs($teacher, 'api');
$this->getJson('/api/v1/print-requests/admin')
->assertForbidden();
}
public function test_teacher_cannot_update_delete_or_download_another_teachers_request(): void
{
$owner = $this->createUserWithRole('teacher');
$intruder = $this->createUserWithRole('teacher');
$request = $this->seedPrintRequest($owner, [
'status' => 'not_assigned',
'file_path' => 'missing-file.pdf',
]);
$this->actingAs($intruder, 'api');
$this->patchJson("/api/v1/print-requests/{$request->id}", [
'num_copies' => 8,
'required_by' => '2026-09-20 11:00:00',
'pickup_method' => 'Self Pickup',
])->assertForbidden();
$this->deleteJson("/api/v1/print-requests/{$request->id}")
->assertForbidden();
$this->getJson("/api/v1/print-requests/{$request->id}/file")
->assertForbidden();
$this->assertDatabaseHas('print_requests', [
'id' => $request->id,
'teacher_id' => $owner->id,
'num_copies' => 1,
]);
}
public function test_teacher_cannot_delete_request_after_processing_has_started(): void
{
$teacher = $this->createUserWithRole('teacher');
$request = $this->seedPrintRequest($teacher, ['status' => 'done']);
$this->actingAs($teacher, 'api');
$this->deleteJson("/api/v1/print-requests/{$request->id}")
->assertBadRequest()
->assertJsonPath('status', false);
$this->assertDatabaseHas('print_requests', [
'id' => $request->id,
'status' => 'done',
]);
}
public function test_upload_requires_valid_file_and_page_selection_format(): void
{
$teacher = $this->createUserWithRole('teacher');
$classSectionId = $this->seedClassSection();
$this->actingAs($teacher, 'api');
$response = $this->post('/api/v1/print-requests', [
'file' => UploadedFile::fake()->create('lesson.pdf', 12, 'application/pdf'),
'class_id' => $classSectionId,
'num_copies' => 1,
'required_by' => '2026-09-13 11:00:00',
'pickup_method' => 'Self Pickup',
'page_selection' => 'one through five',
]);
$response
->assertUnprocessable()
->assertJsonPath('status', false)
->assertJsonStructure(['errors' => ['page_selection']]);
}
public function test_teacher_can_upload_and_download_own_file_and_admin_can_download_any_request_file(): void
{
$teacher = $this->createUserWithRole('teacher');
$admin = $this->createUserWithRole('administrator');
$classSectionId = $this->seedClassSection();
$this->actingAs($teacher, 'api');
$createResponse = $this->post('/api/v1/print-requests', [
'file' => UploadedFile::fake()->create('lesson.pdf', 12, 'application/pdf'),
'class_id' => $classSectionId,
'num_copies' => 2,
'required_by' => '2026-09-13 11:00:00',
'pickup_method' => 'Self Pickup',
'page_selection' => '1-3,5',
]);
$createResponse
->assertCreated()
->assertJsonPath('status', true);
$requestId = (int) $createResponse->json('data.print_request.id');
$this->actingAs($teacher, 'api');
$this->get("/api/v1/print-requests/{$requestId}/file")
->assertOk();
$this->actingAs($admin, 'api');
$this->get("/api/v1/print-requests/{$requestId}/file")
->assertOk();
}
private function createUserWithRole(string $roleName): User
{
$role = Role::query()->firstOrCreate(
['name' => $roleName],
[
'slug' => $roleName,
'description' => ucfirst(str_replace('_', ' ', $roleName)),
'dashboard_route' => $roleName === 'teacher' ? 'teacher/classes' : 'administrator/administratordashboard',
'priority' => 1,
'is_active' => 1,
]
);
$user = User::factory()->create([
'status' => 'Active',
'is_verified' => 1,
'is_suspended' => 0,
'email' => $roleName . '-print-request-' . str_replace('.', '', uniqid('', true)) . '@example.test',
]);
$user->roles()->syncWithoutDetaching([$role->id]);
return $user->fresh() ?? $user;
}
private function seedClassSection(): int
{
DB::table('classSection')->insert([
'class_id' => 1,
'class_section_id' => 9901,
'class_section_name' => 'Print Request Test Class',
'semester' => 'Fall',
'school_year' => '2026-2027',
]);
return 9901;
}
/**
* @param array<string, mixed> $overrides
*/
private function seedPrintRequest(User $teacher, array $overrides = []): PrintRequest
{
return PrintRequest::query()->create(array_merge([
'teacher_id' => $teacher->id,
'admin_id' => null,
'class_id' => $this->seedClassSection(),
'file_path' => '',
'page_selection' => null,
'num_copies' => 1,
'required_by' => '2026-09-13 11:00:00',
'pickup_method' => 'Self Pickup',
'status' => 'not_assigned',
], $overrides));
}
}
@@ -33,7 +33,13 @@ class RoleSwitcherControllerTest extends TestCase
public function test_switch_sets_role(): void
{
$user = $this->seedUser();
$this->seedRole();
$roleId = $this->seedRole();
DB::table('user_roles')->insert([
'user_id' => $user->id,
'role_id' => $roleId,
]);
Sanctum::actingAs($user);
$response = $this->postJson('/api/v1/role-switcher/switch', [
@@ -52,6 +58,55 @@ class RoleSwitcherControllerTest extends TestCase
$response->assertStatus(401);
}
public function test_switch_rejects_role_that_user_does_not_have(): void
{
$user = $this->seedUser();
$this->seedRole();
$teacherRoleId = DB::table('roles')->insertGetId([
'name' => 'teacher',
'slug' => 'teacher',
'description' => 'Teacher',
'dashboard_route' => 'teacher/classes',
'priority' => 2,
'is_active' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
DB::table('user_roles')->insert([
'user_id' => $user->id,
'role_id' => $teacherRoleId,
]);
Sanctum::actingAs($user);
$this->postJson('/api/v1/role-switcher/switch', [
'role' => 'admin',
])
->assertForbidden()
->assertJsonPath('status', false);
}
public function test_switch_validates_empty_role_payload(): void
{
$user = $this->seedUser();
$this->seedRole();
Sanctum::actingAs($user);
$this->postJson('/api/v1/role-switcher/switch', [])
->assertUnprocessable();
}
public function test_index_returns_not_found_when_authenticated_user_has_no_roles(): void
{
$user = $this->seedUser();
Sanctum::actingAs($user);
$this->getJson('/api/v1/role-switcher')
->assertNotFound()
->assertJsonPath('status', false);
}
private function seedUser(): User
{
$userId = DB::table('users')->insertGetId([