postJson('/api/login', [ 'email' => 'missing@example.com', 'password' => 'wrong-password', ])->assertUnauthorized(); } $this->postJson('/api/login', [ 'email' => 'missing@example.com', 'password' => 'wrong-password', ])->assertTooManyRequests(); } public function test_file_upload_rejects_executable_files(): void { Storage::fake('local'); $user = User::factory()->create(['status' => 'active']); $this->grantAdminRole($user); $project = Project::create([ 'title' => 'Security test project', 'project_manager_id' => $user->id, 'created_by' => $user->id, ]); $token = $user->createToken('api-token')->plainTextToken; $this->withToken($token) ->postJson('/api/files', [ 'project_id' => $project->id, 'file' => UploadedFile::fake()->create('payload.php', 4, 'application/x-php'), ]) ->assertUnprocessable(); } public function test_file_resource_does_not_expose_storage_path(): void { $user = User::factory()->create(['status' => 'active']); $this->grantAdminRole($user); $file = File::create([ 'name' => 'safe.pdf', 'original_name' => 'safe.pdf', 'path' => 'files/private-safe.pdf', 'mime_type' => 'application/pdf', 'size' => 123, 'fileable_type' => Project::class, 'fileable_id' => 1, 'user_id' => $user->id, ]); $token = $user->createToken('api-token')->plainTextToken; $this->withToken($token) ->getJson('/api/files') ->assertOk() ->assertJsonMissingPath('data.0.path') ->assertJsonPath('data.0.download_url', url("/api/files/{$file->id}")); } public function test_user_without_permission_cannot_access_user_management(): void { $user = User::factory()->create(['status' => 'active']); $token = $user->createToken('api-token')->plainTextToken; $this->withToken($token) ->getJson('/api/users') ->assertForbidden() ->assertJsonPath('success', false); } public function test_project_permission_does_not_expose_unrelated_projects(): void { $owner = User::factory()->create(['status' => 'active']); $viewer = User::factory()->create(['status' => 'active']); $permission = Permission::create([ 'name' => 'projects.view', 'display_name' => 'View projects', 'guard_name' => 'web', 'module' => 'projects', ]); $role = Role::create(['name' => 'project-viewer', 'display_name' => 'Project viewer', 'guard_name' => 'web']); $role->permissions()->attach($permission); $viewer->roles()->attach($role); $foreignProject = Project::create([ 'title' => 'Private project', 'project_manager_id' => $owner->id, 'created_by' => $owner->id, ]); $token = $viewer->createToken('api-token')->plainTextToken; $this->withToken($token) ->getJson('/api/projects') ->assertOk() ->assertJsonPath('meta.total', 0); $this->withToken($token) ->getJson("/api/projects/{$foreignProject->id}") ->assertForbidden(); } public function test_authenticated_user_cannot_change_another_user_status_without_permission(): void { $actor = User::factory()->create(['status' => 'active']); $target = User::factory()->create(['status' => 'active']); $token = $actor->createToken('api-token')->plainTextToken; $this->withToken($token) ->patchJson("/api/users/{$target->id}/status", ['status' => 'inactive']) ->assertForbidden(); $this->assertSame('active', $target->fresh()->status); } public function test_user_cannot_download_file_from_unrelated_project(): void { $owner = User::factory()->create(['status' => 'active']); $viewer = User::factory()->create(['status' => 'active']); $permission = Permission::create([ 'name' => 'files.view', 'display_name' => 'View files', 'guard_name' => 'web', 'module' => 'files', ]); $role = Role::create(['name' => 'file-viewer', 'display_name' => 'File viewer', 'guard_name' => 'web']); $role->permissions()->attach($permission); $viewer->roles()->attach($role); $project = Project::create([ 'title' => 'Private project', 'project_manager_id' => $owner->id, 'created_by' => $owner->id, ]); $file = File::create([ 'name' => 'private.pdf', 'original_name' => 'private.pdf', 'path' => 'files/private.pdf', 'mime_type' => 'application/pdf', 'size' => 10, 'fileable_type' => Project::class, 'fileable_id' => $project->id, 'user_id' => $owner->id, ]); $token = $viewer->createToken('api-token')->plainTextToken; $this->withToken($token)->getJson("/api/files/{$file->id}")->assertForbidden(); } public function test_meeting_action_item_must_belong_to_meeting_in_route(): void { $user = User::factory()->create(['status' => 'active']); $this->grantAdminRole($user); $project = Project::create([ 'title' => 'Meeting security', 'project_manager_id' => $user->id, 'created_by' => $user->id, ]); $first = Meeting::create([ 'title' => 'First', 'project_id' => $project->id, 'date' => now()->toDateString(), 'meeting_type' => 'other', 'created_by' => $user->id, ]); $second = Meeting::create([ 'title' => 'Second', 'project_id' => $project->id, 'date' => now()->toDateString(), 'meeting_type' => 'other', 'created_by' => $user->id, ]); $item = MeetingActionItem::create([ 'meeting_id' => $second->id, 'title' => 'Private action', ]); $token = $user->createToken('api-token')->plainTextToken; $this->withToken($token) ->putJson("/api/meetings/{$first->id}/action-items/{$item->id}", ['title' => 'Tampered']) ->assertForbidden(); $this->assertSame('Private action', $item->fresh()->title); } }