Objectif
Écrire une suite de tests complète pour l'API du mini-projet 06 : tests unitaires sur les models, tests de feature sur tous les endpoints, mocking Mail/Queue, et couverture ≥ 80 %.
| Type | Cible | Nombre |
|---|---|---|
| Unit | Task model (scopes, is_overdue, accesseurs) | 5+ |
| Feature | Tous les endpoints CRUD de TaskController | 10+ |
| Feature | AuthController (register/login/logout) | 5+ |
| Mock | Mail fake sur création de tâche urgente | 2+ |
Checklist des tests à écrire
- ✅
test_task_is_overdue_when_due_date_is_past - ✅
test_task_is_not_overdue_when_status_is_done - ✅
test_authenticated_user_can_list_their_tasks - ✅
test_tasks_are_filtered_by_status - ✅
test_tasks_are_filtered_by_priority - ✅
test_user_can_create_a_task - ✅
test_task_creation_fails_without_title - ✅
test_user_can_only_see_their_own_tasks - ✅
test_user_can_update_their_task - ✅
test_user_cannot_update_another_users_task - ✅
test_user_can_delete_their_task - ✅
test_register_creates_user_and_returns_token - ✅
test_login_fails_with_wrong_password - ✅
test_logout_revokes_current_token
Solution commentée
// tests/Unit/Models/TaskTest.php
namespace Tests\Unit\Models;
use App\Models\Task;
use Carbon\Carbon;
use PHPUnit\Framework\TestCase;
class TaskTest extends TestCase
{
public function test_task_is_overdue_when_due_date_is_past(): void {
$task = new Task([
'due_date' => Carbon::yesterday(),
'status' => 'todo',
]);
$this->assertTrue($task->is_overdue);
}
public function test_task_is_not_overdue_when_status_is_done(): void {
$task = new Task([
'due_date' => Carbon::yesterday(),
'status' => 'done',
]);
$this->assertFalse($task->is_overdue);
}
public function test_task_is_not_overdue_when_no_due_date(): void {
$task = new Task(['due_date' => null, 'status' => 'todo']);
$this->assertFalse($task->is_overdue);
}
}
// tests/Feature/Api/TaskTest.php
namespace Tests\Feature\Api;
use App\Models\Task;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class TaskTest extends TestCase
{
use RefreshDatabase;
private User $user;
private string $token;
protected function setUp(): void {
parent::setUp();
$this->user = User::factory()->create();
}
public function test_authenticated_user_can_list_their_tasks(): void {
Task::factory()->count(5)->create(['user_id' => $this->user->id]);
Task::factory()->count(3)->create(); // autres utilisateurs
$this->actingAs($this->user)
->getJson('/api/v1/tasks')
->assertOk()
->assertJsonCount(5, 'data'); // seulement ses tâches
}
public function test_tasks_are_filtered_by_status(): void {
Task::factory()->count(3)->create(['user_id' => $this->user->id, 'status' => 'todo']);
Task::factory()->count(2)->create(['user_id' => $this->user->id, 'status' => 'done']);
$this->actingAs($this->user)
->getJson('/api/v1/tasks?status=todo')
->assertOk()
->assertJsonCount(3, 'data');
}
public function test_user_can_create_a_task(): void {
$this->actingAs($this->user)
->postJson('/api/v1/tasks', [
'title' => 'Implement login',
'priority' => 'high',
'status' => 'todo',
])
->assertCreated()
->assertJsonPath('data.title', 'Implement login');
$this->assertDatabaseHas('tasks', [
'title' => 'Implement login',
'user_id' => $this->user->id,
]);
}
public function test_task_creation_fails_without_title(): void {
$this->actingAs($this->user)
->postJson('/api/v1/tasks', ['priority' => 'high'])
->assertUnprocessable()
->assertJsonValidationErrors(['title']);
}
public function test_user_cannot_update_another_users_task(): void {
$other = User::factory()->create();
$task = Task::factory()->create(['user_id' => $other->id]);
$this->actingAs($this->user)
->putJson("/api/v1/tasks/{$task->id}", ['title' => 'Hacked'])
->assertForbidden();
}
public function test_user_can_delete_their_task(): void {
$task = Task::factory()->create(['user_id' => $this->user->id]);
$this->actingAs($this->user)
->deleteJson("/api/v1/tasks/{$task->id}")
->assertNoContent();
$this->assertDatabaseMissing('tasks', ['id' => $task->id]);
}
}
// tests/Feature/Api/AuthTest.php
class AuthTest extends TestCase
{
use RefreshDatabase;
public function test_register_creates_user_and_returns_token(): void {
$this->postJson('/api/v1/auth/register', [
'name' => 'Test User',
'email' => 'test@example.com',
'password' => 'password123',
'password_confirmation' => 'password123',
])
->assertCreated()
->assertJsonStructure(['token', 'token_type']);
$this->assertDatabaseHas('users', ['email' => 'test@example.com']);
}
public function test_login_fails_with_wrong_password(): void {
User::factory()->create(['email' => 'test@example.com']);
$this->postJson('/api/v1/auth/login', [
'email' => 'test@example.com',
'password' => 'wrongpassword',
])->assertUnauthorized();
}
public function test_logout_revokes_current_token(): void {
$user = User::factory()->create();
$this->actingAs($user)
->postJson('/api/v1/auth/logout')
->assertOk();
// Token révoqué — plus d'accès
$this->actingAs($user)
->getJson('/api/v1/tasks')
->assertUnauthorized();
}
}