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 %.

TypeCibleNombre
UnitTask model (scopes, is_overdue, accesseurs)5+
FeatureTous les endpoints CRUD de TaskController10+
FeatureAuthController (register/login/logout)5+
MockMail fake sur création de tâche urgente2+

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();
    }
}
← Cours Module 07 🧠 QCM Module 08 →