$name, 'guard_name' => 'web']); } private function userWithRole(string $role, ?Service $service = null): User { $service ??= Service::factory()->create(); $user = User::factory()->create(['service_id' => $service->id, 'email_verified_at' => now()]); $user->assignRole($this->createRole($role)); return $user; } private function makeBudget(Service $service, string $statut = 'preparation'): Budget { return Budget::create([ 'service_id' => $service->id, 'annee' => 2026, 'type_budget' => 'agglo', 'statut' => $statut, ]); } private function makeLigne(Budget $budget): LigneBudget { return LigneBudget::create([ 'budget_id' => $budget->id, 'nom' => 'Ligne test', 'type_depense' => 'investissement', 'montant_propose' => 1000, ]); } // ------------------------------------------------------------------------- // LigneBudgetController::destroy — faille critique // ------------------------------------------------------------------------- public function test_responsable_can_delete_ligne_of_own_service(): void { $service = Service::factory()->create(); $user = $this->userWithRole('responsable', $service); $budget = $this->makeBudget($service); $ligne = $this->makeLigne($budget); $response = $this->actingAs($user)->delete(route('lignes-budget.destroy', $ligne)); $response->assertRedirect(); $this->assertDatabaseMissing('ligne_budgets', ['id' => $ligne->id]); } public function test_responsable_cannot_delete_ligne_of_another_service(): void { $otherService = Service::factory()->create(); $myService = Service::factory()->create(); $user = $this->userWithRole('responsable', $myService); $budget = $this->makeBudget($otherService); $ligne = $this->makeLigne($budget); $response = $this->actingAs($user)->delete(route('lignes-budget.destroy', $ligne)); $response->assertForbidden(); $this->assertDatabaseHas('ligne_budgets', ['id' => $ligne->id]); } public function test_unauthenticated_user_cannot_delete_ligne(): void { $service = Service::factory()->create(); $budget = $this->makeBudget($service); $ligne = $this->makeLigne($budget); $response = $this->delete(route('lignes-budget.destroy', $ligne)); $response->assertRedirect(route('login')); $this->assertDatabaseHas('ligne_budgets', ['id' => $ligne->id]); } public function test_cannot_delete_ligne_when_budget_is_locked(): void { $service = Service::factory()->create(); $user = $this->userWithRole('responsable', $service); $budget = $this->makeBudget($service, 'valide'); $ligne = $this->makeLigne($budget); $response = $this->actingAs($user)->delete(route('lignes-budget.destroy', $ligne)); $response->assertForbidden(); $this->assertDatabaseHas('ligne_budgets', ['id' => $ligne->id]); } public function test_admin_can_delete_ligne_of_any_service(): void { $otherService = Service::factory()->create(); $admin = $this->userWithRole('admin'); $budget = $this->makeBudget($otherService); $ligne = $this->makeLigne($budget); $response = $this->actingAs($admin)->delete(route('lignes-budget.destroy', $ligne)); $response->assertRedirect(); $this->assertDatabaseMissing('ligne_budgets', ['id' => $ligne->id]); } // ------------------------------------------------------------------------- // LigneBudgetController::update — vérification service_id // ------------------------------------------------------------------------- public function test_responsable_can_update_ligne_of_own_service(): void { $service = Service::factory()->create(); $user = $this->userWithRole('responsable', $service); $budget = $this->makeBudget($service); $ligne = $this->makeLigne($budget); $response = $this->actingAs($user)->put(route('lignes-budget.update', $ligne), [ 'nom' => 'Ligne modifiée', 'type_depense' => 'fonctionnement', 'montant_propose' => 2000, ]); $response->assertRedirect(); $this->assertDatabaseHas('ligne_budgets', ['id' => $ligne->id, 'nom' => 'Ligne modifiée']); } public function test_responsable_cannot_update_ligne_of_another_service(): void { $otherService = Service::factory()->create(); $myService = Service::factory()->create(); $user = $this->userWithRole('responsable', $myService); $budget = $this->makeBudget($otherService); $ligne = $this->makeLigne($budget); $response = $this->actingAs($user)->put(route('lignes-budget.update', $ligne), [ 'nom' => 'Tentative de modification', 'type_depense' => 'fonctionnement', 'montant_propose' => 9999, ]); $response->assertForbidden(); $this->assertDatabaseHas('ligne_budgets', ['id' => $ligne->id, 'nom' => 'Ligne test']); } public function test_cannot_update_ligne_when_budget_is_locked(): void { $service = Service::factory()->create(); $user = $this->userWithRole('responsable', $service); $budget = $this->makeBudget($service, 'arbitrage_direction'); $ligne = $this->makeLigne($budget); $response = $this->actingAs($user)->put(route('lignes-budget.update', $ligne), [ 'nom' => 'Tentative modif budget verrouillé', 'type_depense' => 'investissement', 'montant_propose' => 5000, ]); $response->assertForbidden(); } // ------------------------------------------------------------------------- // LigneBudgetController::store — vérification service_id // ------------------------------------------------------------------------- public function test_responsable_can_add_ligne_to_own_budget(): void { $service = Service::factory()->create(); $user = $this->userWithRole('responsable', $service); $budget = $this->makeBudget($service); $response = $this->actingAs($user)->post(route('lignes-budget.store'), [ 'budget_id' => $budget->id, 'nom' => 'Nouvelle ligne', 'type_depense' => 'investissement', 'montant_propose' => 500, ]); $response->assertRedirect(); $this->assertDatabaseHas('ligne_budgets', ['budget_id' => $budget->id, 'nom' => 'Nouvelle ligne']); } public function test_responsable_cannot_add_ligne_to_another_service_budget(): void { $otherService = Service::factory()->create(); $myService = Service::factory()->create(); $user = $this->userWithRole('responsable', $myService); $budget = $this->makeBudget($otherService); $response = $this->actingAs($user)->post(route('lignes-budget.store'), [ 'budget_id' => $budget->id, 'nom' => 'Ligne injectée', 'type_depense' => 'investissement', 'montant_propose' => 500, ]); $response->assertForbidden(); $this->assertDatabaseMissing('ligne_budgets', ['budget_id' => $budget->id]); } // ------------------------------------------------------------------------- // BudgetController::update — statut réservé aux admin/directeur // ------------------------------------------------------------------------- public function test_directeur_can_change_budget_status(): void { $service = Service::factory()->create(); $user = $this->userWithRole('directeur', $service); $budget = $this->makeBudget($service, 'preparation'); $response = $this->actingAs($user)->put(route('budgets.update', $budget), [ 'statut' => 'arbitrage_dsi', ]); $response->assertRedirect(); $this->assertDatabaseHas('budgets', ['id' => $budget->id, 'statut' => 'arbitrage_dsi']); } public function test_responsable_cannot_change_budget_status(): void { $service = Service::factory()->create(); $user = $this->userWithRole('responsable', $service); $budget = $this->makeBudget($service, 'preparation'); $response = $this->actingAs($user)->put(route('budgets.update', $budget), [ 'statut' => 'valide', ]); $response->assertForbidden(); $this->assertDatabaseHas('budgets', ['id' => $budget->id, 'statut' => 'preparation']); } public function test_acheteur_cannot_change_budget_status(): void { $service = Service::factory()->create(); $user = $this->userWithRole('acheteur', $service); $budget = $this->makeBudget($service, 'preparation'); $response = $this->actingAs($user)->put(route('budgets.update', $budget), [ 'statut' => 'valide', ]); $response->assertForbidden(); } // ------------------------------------------------------------------------- // BudgetController exports — validation des paramètres enum // ------------------------------------------------------------------------- public function test_export_excel_ignores_invalid_type_parameter(): void { $admin = $this->userWithRole('admin'); // Paramètre type invalide — ne doit pas causer d'erreur ni être utilisé $response = $this->actingAs($admin)->get(route('budgets.export.excel', [ 'annee' => 2026, 'type' => 'INJECTED_VALUE; DROP TABLE budgets;', ])); // Doit retourner le fichier (200) sans planter $response->assertOk(); } public function test_export_pdf_ignores_invalid_envelope_parameter(): void { $admin = $this->userWithRole('admin'); $response = $this->actingAs($admin)->get(route('budgets.export.pdf', [ 'annee' => 2026, 'envelope' => 'invalid_value', ])); $response->assertOk(); } public function test_export_accepts_valid_type_parameter(): void { $admin = $this->userWithRole('admin'); $response = $this->actingAs($admin)->get(route('budgets.export.excel', [ 'annee' => 2026, 'type' => 'investissement', ])); $response->assertOk(); } public function test_export_accepts_valid_envelope_parameter(): void { $admin = $this->userWithRole('admin'); $response = $this->actingAs($admin)->get(route('budgets.export.pdf', [ 'annee' => 2026, 'envelope' => 'agglo', ])); $response->assertOk(); } // ------------------------------------------------------------------------- // Arbitrage — réservé aux admin/directeur/raf // ------------------------------------------------------------------------- public function test_raf_can_arbitrer_ligne(): void { $service = Service::factory()->create(); $user = $this->userWithRole('raf', $service); $budget = $this->makeBudget($service); $ligne = $this->makeLigne($budget); $response = $this->actingAs($user)->patch(route('lignes-budget.arbitrer', $ligne), [ 'statut_arbitrage' => 'accepte_dsi', 'montant_arbitre' => 800, ]); $response->assertRedirect(); $this->assertDatabaseHas('ligne_budgets', [ 'id' => $ligne->id, 'statut_arbitrage' => 'accepte_dsi', 'montant_arbitre' => 800, ]); } public function test_responsable_cannot_arbitrer_ligne(): void { $service = Service::factory()->create(); $user = $this->userWithRole('responsable', $service); $budget = $this->makeBudget($service); $ligne = $this->makeLigne($budget); $response = $this->actingAs($user)->patch(route('lignes-budget.arbitrer', $ligne), [ 'statut_arbitrage' => 'valide_definitif', 'montant_arbitre' => 1000, ]); $response->assertForbidden(); $this->assertDatabaseHas('ligne_budgets', [ 'id' => $ligne->id, 'statut_arbitrage' => 'brouillon', ]); } }