PM_Console/backend/tests/Feature/SecurityHardeningTest.php

235 خطوط
8.0 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\File;
use App\Models\Meeting;
use App\Models\MeetingActionItem;
use App\Models\Permission;
use App\Models\Project;
use App\Models\Role;
use App\Models\Task;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class SecurityHardeningTest extends TestCase
{
use RefreshDatabase;
public function test_login_is_rate_limited(): void
{
for ($i = 0; $i < 5; $i++) {
$this->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);
}
public function test_dashboard_monitoring_uses_task_blocker_fields(): void
{
$user = User::factory()->create(['status' => 'active']);
$this->grantAdminRole($user);
$project = Project::create([
'title' => 'Monitoring project',
'project_manager_id' => $user->id,
'created_by' => $user->id,
]);
Task::create([
'title' => 'Blocked task',
'project_id' => $project->id,
'reporter_id' => $user->id,
'created_by' => $user->id,
'status' => 'in_progress',
'blocker_type' => 'dependency',
]);
$token = $user->createToken('api-token')->plainTextToken;
$this->withToken($token)
->getJson('/api/dashboard/monitoring')
->assertOk()
->assertJsonPath('success', true)
->assertJsonPath('data.summary.blocked_tasks', 1)
->assertJsonPath('data.projects.0.blocked_tasks', 1);
}
}