================================================================================
[OK] SONARQUBE EN ÉQUIPE DE 3 DÉVELOPPEURS - APPLICATION JAVA / SPRING BOOT (GUIDE ULTRA-DÉTAILLÉ)
================================================================================

Ce guide couvre un cas réel et concret: une équipe de 3 développeurs Java
qui travaillent ensemble sur une application Spring Boot REST API et utilisent
SonarQube pour maintenir la qualité du code collectivement.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PRÉSENTATION DE L'ÉQUIPE ET DU PROJET
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

L'équipe:
  - Alice  -> Lead développeuse (aussi admin SonarQube)
  - Bob    -> Développeur backend
  - Claire -> Développeuse backend + tests

Le projet:
  - Nom: "TaskManager API"
  - Stack: Spring Boot 3 + Spring Data JPA + H2 (tests) / PostgreSQL (prod)
  - Build: Maven
  - Coverage: JaCoCo
  - Versionning: Git + GitHub
  - Dépôt: github.com/equipe/taskmanager-java

Application Spring Boot REST:
  - GET    /api/tasks          -> lister les tâches
  - POST   /api/tasks          -> créer une tâche
  - GET    /api/tasks/{id}     -> récupérer une tâche
  - PUT    /api/tasks/{id}     -> modifier une tâche
  - DELETE /api/tasks/{id}     -> supprimer une tâche
  - GET    /api/tasks/filter   -> filtrer par statut

Structure du projet (Maven standard):
  taskmanager-java/
  ├── src/
  │   ├── main/
  │   │   ├── java/
  │   │   │   └── com/equipe/taskmanager/
  │   │   │       ├── TaskmanagerApplication.java
  │   │   │       ├── controller/
  │   │   │       │   └── TaskController.java
  │   │   │       ├── service/
  │   │   │       │   └── TaskService.java
  │   │   │       ├── repository/
  │   │   │       │   └── TaskRepository.java
  │   │   │       ├── model/
  │   │   │       │   └── Task.java
  │   │   │       └── exception/
  │   │   │           └── TaskNotFoundException.java
  │   │   └── resources/
  │   │       └── application.properties
  │   └── test/
  │       └── java/
  │           └── com/equipe/taskmanager/
  │               ├── controller/
  │               │   └── TaskControllerTest.java
  │               └── service/
  │                   └── TaskServiceTest.java
  ├── pom.xml
  ├── sonar-project.properties
  └── .gitignore


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 1 - MISE EN PLACE PAR ALICE (ADMIN / LEAD DEV)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Alice est chargée de mettre en place SonarQube pour toute l'équipe.
Elle fait cela une seule fois. Bob et Claire n'ont qu'à suivre la PARTIE 2.

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.1 - Alice installe SonarQube avec Docker
────────────────────────────────────────────────────────────────────────────────

Alice ouvre son terminal et lance SonarQube:

  COMMANDE:
  ─────────
  docker run -d \
    --name sonarqube \
    --restart unless-stopped \
    -p 9000:9000 \
    -v sonarqube_data:/opt/sonarqube/data \
    -v sonarqube_logs:/opt/sonarqube/logs \
    -v sonarqube_extensions:/opt/sonarqube/extensions \
    sonarqube:lts-community

  Attendre 2-3 minutes, puis vérifier:

  COMMANDE:
  ─────────
  curl http://localhost:9000/api/system/status

  RÉSULTAT ATTENDU:
  ─────────────────
  {"status":"UP"}

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.2 - Alice se connecte et change le mot de passe admin
────────────────────────────────────────────────────────────────────────────────

INTERFACE WEB:

  1. Ouvrir: http://localhost:9000
  2. Page de connexion:
     ┌─────────────────────────────────────┐
     │  [BLEU] SonarQube                       │
     │                                     │
     │  Username: [admin          ]        │
     │  Password: [admin          ]        │
     │                                     │
     │  [      Log In      ]               │
     └─────────────────────────────────────┘

  3. Entrer: admin / admin  -> Cliquer "Log In"

  4. SonarQube demande de changer le mot de passe:
     Old password: admin
     New password: TaskManager2024!
     Confirm:      TaskManager2024!
     -> Cliquer "Update"

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.3 - Alice crée les comptes pour Bob et Claire
────────────────────────────────────────────────────────────────────────────────

INTERFACE WEB:

  Chemin: Administration > Security > Users > [+ Create User]

  Créer compte Bob:
  ┌─────────────────────────────────────────────────┐
  │  Create User                                    │
  │                                                 │
  │  Login*:    [bob                ]               │
  │  Name*:     [Bob Dupont         ]               │
  │  Email:     [bob@equipe.com     ]               │
  │  Password*: [BobPass2024!       ]               │
  │                                                 │
  │  [   Create   ]                                 │
  └─────────────────────────────────────────────────┘

  Créer compte Claire (même procédure):
  Login: claire | Name: Claire Martin | Password: ClairePass2024!

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.4 - Alice crée le groupe "taskmanager-devs" et y ajoute Bob et Claire
────────────────────────────────────────────────────────────────────────────────

INTERFACE WEB:

  1. Chemin: Administration > Security > Groups > [+ Create Group]
     ┌─────────────────────────────────────────────────┐
     │  Create Group                                   │
     │  Name*:        [taskmanager-devs   ]            │
     │  Description:  [Equipe Java TaskManager]        │
     │  [   Create   ]                                 │
     └─────────────────────────────────────────────────┘

  2. Dans la liste, cliquer "Members" à droite de "taskmanager-devs"
  3. Ajouter bob et claire au groupe (cocher + "Add selected")

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.5 - Alice crée le projet SonarQube
────────────────────────────────────────────────────────────────────────────────

INTERFACE WEB:

  1. Chemin: Projects > [+ Create project]
  2. Choisir "Manually"
  3. Remplir le formulaire:
     ┌─────────────────────────────────────────────────┐
     │  Create a project manually                      │
     │                                                 │
     │  Project display name*:                         │
     │  [TaskManager Java API             ]            │
     │                                                 │
     │  Project key*:                                  │
     │  [taskmanager-java-api             ]            │
     │                                                 │
     │  Main branch name:                              │
     │  [main                             ]            │
     │                                                 │
     │  [         Next         ]                       │
     └─────────────────────────────────────────────────┘

  4. Cliquer "Next"
  5. Choisir "Locally"

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.6 - Alice génère le token du projet
────────────────────────────────────────────────────────────────────────────────

INTERFACE WEB (suite):

  6. SonarQube affiche:
     ┌──────────────────────────────────────────────────────┐
     │  Provide a token                                     │
     │                                                      │
     │  Token name*: [taskmanager-java-token       ]        │
     │  Token type:  [BLACK_CIRCLE] Project Analysis Token               │
     │  Expires in:  [No expiration          [BLACK_DOWN-POINTING_TRIANGLE]]            │
     │                                                      │
     │  [         Generate         ]                        │
     └──────────────────────────────────────────────────────┘

  7. Cliquer "Generate"

  8. Token affiché:
     ┌──────────────────────────────────────────────────────┐
     │  [OK] Token generated successfully!                    │
     │                                                      │
     │  squ_j4v4e2c1d9b3f7a5e8c2d4f1b6e9a3c7d5f2b8e1     │
     │                                                      │
     │  [ATTENTION]  Save this token! It won't be shown again.      │
     │  [         Continue         ]                        │
     └──────────────────────────────────────────────────────┘

  9. Alice copie et note le token dans un endroit sûr.
     Token: squ_j4v4e2c1d9b3f7a5e8c2d4f1b6e9a3c7d5f2b8e1

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.7 - Alice configure les permissions du projet
────────────────────────────────────────────────────────────────────────────────

INTERFACE WEB:

  Chemin: Projects > TaskManager Java API > Project Settings > Permissions

  Chercher le groupe "taskmanager-devs" et cocher:
  [OK] Browse           (voir le projet)
  [OK] See Source Code  (voir le code)
  [OK] Execute Analysis (lancer des analyses)
  -> Cliquer "Grant"

  RÉSULTAT:
  ┌───────────────────────┬────────┬──────────┬─────────┬───────────┐
  │  Group/User           │ Browse │ See Code │ Execute │ Administer│
  ├───────────────────────┼────────┼──────────┼─────────┼───────────┤
  │  taskmanager-devs     │  [OK]     │    [OK]     │    [OK]    │           │
  └───────────────────────┴────────┴──────────┴─────────┴───────────┘

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.8 - Alice crée la Quality Gate de l'équipe
────────────────────────────────────────────────────────────────────────────────

INTERFACE WEB:

  1. Chemin: Administration > Quality Gates > [Create]
     Name: "TaskManager-Java-QG"

  2. Ajouter les conditions (cliquer "Add Condition" pour chacune):

     Condition 1:
     Where: On New Code | Metric: Coverage | is less than | 80
     -> [Add Condition]

     Condition 2:
     Where: On New Code | Metric: Bugs | is greater than | 0
     -> [Add Condition]

     Condition 3:
     Where: On New Code | Metric: Vulnerabilities | is greater than | 0
     -> [Add Condition]

     Condition 4:
     Where: On New Code | Metric: Duplicated Lines (%) | is greater than | 3
     -> [Add Condition]

     Condition 5:
     Where: On New Code | Metric: Security Hotspots Reviewed | is less than | 100
     -> [Add Condition]

  3. Quality Gate finale:
     ┌───────────────────────────────────────────────────────────────┐
     │  TaskManager-Java-QG                                          │
     ├──────────────────────────────────┬───────────────────────────┤
     │  Condition                       │  Seuil                    │
     ├──────────────────────────────────┼───────────────────────────┤
     │  New Coverage                    │  < 80%  -> FAIL            │
     │  New Bugs                        │  > 0    -> FAIL            │
     │  New Vulnerabilities             │  > 0    -> FAIL            │
     │  New Duplicated Lines (%)        │  > 3%   -> FAIL            │
     │  New Security Hotspots Reviewed  │  < 100% -> FAIL            │
     └──────────────────────────────────┴───────────────────────────┘

  4. Appliquer au projet:
     Projects > TaskManager Java API > Project Settings > Quality Gate
     Sélectionner: "TaskManager-Java-QG"
     -> [Save]

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.9 - Alice crée le projet Maven avec Spring Boot
────────────────────────────────────────────────────────────────────────────────

Alice génère le projet avec Spring Initializr (start.spring.io) ou en ligne de commande.

  OPTION A - Via le site web Spring Initializr:
  ──────────────────────────────────────────────
  1. Aller sur: https://start.spring.io
  2. Configurer:
     ┌──────────────────────────────────────────────────────────────┐
     │  Project:    [BLACK_CIRCLE] Maven                                         │
     │  Language:   [BLACK_CIRCLE] Java                                          │
     │  Spring Boot: 3.2.0                                          │
     │                                                              │
     │  Group:    com.equipe                                        │
     │  Artifact: taskmanager                                       │
     │  Name:     taskmanager                                       │
     │  Package:  com.equipe.taskmanager                            │
     │  Java:     17                                                │
     │                                                              │
     │  Dependencies:                                               │
     │  [OK] Spring Web                                                │
     │  [OK] Spring Data JPA                                           │
     │  [OK] H2 Database                                               │
     │  [OK] Validation                                                │
     │  [OK] Lombok                                                    │
     └──────────────────────────────────────────────────────────────┘
  3. Cliquer "Generate" -> Télécharge un .zip
  4. Décompresser dans un dossier de travail

  OPTION B - Maven en ligne de commande:
  ───────────────────────────────────────
  mvn archetype:generate \
    -DgroupId=com.equipe \
    -DartifactId=taskmanager \
    -DarchetypeArtifactId=maven-archetype-quickstart \
    -DinteractiveMode=false

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.10 - Alice configure le pom.xml avec JaCoCo et SonarQube
────────────────────────────────────────────────────────────────────────────────

C'est LE fichier le plus important pour l'intégration SonarQube + Java.
Alice édite pom.xml:

  ── pom.xml COMPLET ──────────────────────────────────────────────────────────

  <?xml version="1.0" encoding="UTF-8"?>
  <project xmlns="http://maven.apache.org/POM/4.0.0"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
           https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>3.2.0</version>
      <relativePath/>
    </parent>

    <groupId>com.equipe</groupId>
    <artifactId>taskmanager</artifactId>
    <version>1.0.0</version>
    <name>TaskManager Java API</name>
    <description>API REST de gestion de tâches en équipe</description>

    <properties>
      <java.version>17</java.version>

      <!-- ===== SONARQUBE PROPERTIES ===== -->
      <!-- URL du serveur SonarQube -->
      <sonar.host.url>http://localhost:9000</sonar.host.url>

      <!-- Clé du projet dans SonarQube (doit correspondre à l'étape 1.5) -->
      <sonar.projectKey>taskmanager-java-api</sonar.projectKey>
      <sonar.projectName>TaskManager Java API</sonar.projectName>

      <!-- Chemin vers le rapport JaCoCo (généré automatiquement) -->
      <sonar.coverage.jacoco.xmlReportPaths>
        ${project.build.directory}/site/jacoco/jacoco.xml
      </sonar.coverage.jacoco.xmlReportPaths>

      <!-- Fichiers à exclure de l'analyse SonarQube -->
      <sonar.exclusions>
        **/TaskmanagerApplication.java,
        **/model/**,
        **/exception/**
      </sonar.exclusions>

      <!-- Fichiers à exclure du calcul de couverture -->
      <sonar.coverage.exclusions>
        **/TaskmanagerApplication.java,
        **/model/**,
        **/exception/**
      </sonar.coverage.exclusions>
    </properties>

    <dependencies>
      <!-- Spring Boot Web (REST API) -->
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
      </dependency>

      <!-- Spring Data JPA (accès base de données) -->
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
      </dependency>

      <!-- Bean Validation (@NotNull, @Size, etc.) -->
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
      </dependency>

      <!-- H2 Database (base en mémoire pour tests) -->
      <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
      </dependency>

      <!-- Lombok (réduction de boilerplate) -->
      <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
      </dependency>

      <!-- Spring Boot Test (JUnit 5, Mockito, MockMvc) -->
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
      </dependency>
    </dependencies>

    <build>
      <plugins>
        <!-- Plugin Spring Boot pour créer le JAR exécutable -->
        <plugin>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-maven-plugin</artifactId>
          <configuration>
            <excludes>
              <exclude>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
              </exclude>
            </excludes>
          </configuration>
        </plugin>

        <!-- ===== PLUGIN JACOCO - COVERAGE ===== -->
        <!-- JaCoCo mesure le % de code testé et génère le rapport XML pour SonarQube -->
        <plugin>
          <groupId>org.jacoco</groupId>
          <artifactId>jacoco-maven-plugin</artifactId>
          <version>0.8.11</version>
          <executions>
            <!-- Phase 1: Prépare l'agent JaCoCo AVANT les tests -->
            <execution>
              <id>prepare-agent</id>
              <goals>
                <goal>prepare-agent</goal>
              </goals>
            </execution>

            <!-- Phase 2: Génère le rapport XML APRÈS les tests -->
            <execution>
              <id>report</id>
              <phase>test</phase>
              <goals>
                <goal>report</goal>
              </goals>
            </execution>

            <!-- Phase 3 (optionnel): Vérifie le seuil de coverage -->
            <!-- Si coverage < 80%, Maven fail avec erreur claire -->
            <execution>
              <id>check</id>
              <goals>
                <goal>check</goal>
              </goals>
              <configuration>
                <rules>
                  <rule>
                    <element>BUNDLE</element>
                    <limits>
                      <limit>
                        <counter>LINE</counter>
                        <value>COVEREDRATIO</value>
                        <minimum>0.80</minimum>
                      </limit>
                    </limits>
                  </rule>
                </rules>
              </configuration>
            </execution>
          </executions>
        </plugin>

        <!-- ===== PLUGIN SONARQUBE ===== -->
        <plugin>
          <groupId>org.sonarsource.scanner.maven</groupId>
          <artifactId>sonar-maven-plugin</artifactId>
          <version>3.10.0.2594</version>
        </plugin>
      </plugins>
    </build>

  </project>

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.11 - Alice crée les fichiers source Java de base
────────────────────────────────────────────────────────────────────────────────

  ── model/Task.java ──────────────────────────────────────────────────────────

  package com.equipe.taskmanager.model;

  import jakarta.persistence.*;
  import jakarta.validation.constraints.NotBlank;
  import jakarta.validation.constraints.Size;
  import lombok.Data;
  import lombok.NoArgsConstructor;
  import lombok.AllArgsConstructor;
  import java.time.LocalDateTime;

  @Entity
  @Table(name = "tasks")
  @Data
  @NoArgsConstructor
  @AllArgsConstructor
  public class Task {

      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id;

      @NotBlank(message = "Le titre est obligatoire")
      @Size(min = 1, max = 200, message = "Le titre doit faire entre 1 et 200 caractères")
      @Column(nullable = false)
      private String title;

      @Column(columnDefinition = "TEXT")
      private String description;

      @Column(nullable = false)
      private Boolean done = false;

      @Column(name = "created_at", nullable = false, updatable = false)
      private LocalDateTime createdAt;

      @PrePersist
      protected void onCreate() {
          this.createdAt = LocalDateTime.now();
      }
  }

  ── exception/TaskNotFoundException.java ─────────────────────────────────────

  package com.equipe.taskmanager.exception;

  import org.springframework.http.HttpStatus;
  import org.springframework.web.bind.annotation.ResponseStatus;

  @ResponseStatus(HttpStatus.NOT_FOUND)
  public class TaskNotFoundException extends RuntimeException {

      public TaskNotFoundException(Long id) {
          super("Tâche introuvable avec l'ID: " + id);
      }
  }

  ── repository/TaskRepository.java ───────────────────────────────────────────

  package com.equipe.taskmanager.repository;

  import com.equipe.taskmanager.model.Task;
  import org.springframework.data.jpa.repository.JpaRepository;
  import org.springframework.stereotype.Repository;
  import java.util.List;

  @Repository
  public interface TaskRepository extends JpaRepository<Task, Long> {

      // Trouver les tâches par statut (done = true ou false)
      List<Task> findByDone(Boolean done);

      // Recherche insensible à la casse dans le titre
      List<Task> findByTitleContainingIgnoreCase(String keyword);
  }

  ── service/TaskService.java ─────────────────────────────────────────────────

  package com.equipe.taskmanager.service;

  import com.equipe.taskmanager.exception.TaskNotFoundException;
  import com.equipe.taskmanager.model.Task;
  import com.equipe.taskmanager.repository.TaskRepository;
  import lombok.RequiredArgsConstructor;
  import org.springframework.stereotype.Service;
  import org.springframework.transaction.annotation.Transactional;
  import java.util.List;

  @Service
  @RequiredArgsConstructor
  public class TaskService {

      private final TaskRepository taskRepository;

      public List<Task> getAllTasks() {
          return taskRepository.findAll();
      }

      public Task getTaskById(Long id) {
          return taskRepository.findById(id)
                  .orElseThrow(() -> new TaskNotFoundException(id));
      }

      @Transactional
      public Task createTask(Task task) {
          return taskRepository.save(task);
      }

      @Transactional
      public Task updateTask(Long id, Task taskDetails) {
          Task task = getTaskById(id);
          task.setTitle(taskDetails.getTitle());
          task.setDescription(taskDetails.getDescription());
          task.setDone(taskDetails.getDone());
          return taskRepository.save(task);
      }

      @Transactional
      public void deleteTask(Long id) {
          Task task = getTaskById(id);
          taskRepository.delete(task);
      }

      public List<Task> filterByStatus(Boolean done) {
          return taskRepository.findByDone(done);
      }
  }

  ── controller/TaskController.java ───────────────────────────────────────────

  package com.equipe.taskmanager.controller;

  import com.equipe.taskmanager.model.Task;
  import com.equipe.taskmanager.service.TaskService;
  import jakarta.validation.Valid;
  import lombok.RequiredArgsConstructor;
  import org.springframework.http.HttpStatus;
  import org.springframework.http.ResponseEntity;
  import org.springframework.web.bind.annotation.*;
  import java.util.List;

  @RestController
  @RequestMapping("/api/tasks")
  @RequiredArgsConstructor
  public class TaskController {

      private final TaskService taskService;

      @GetMapping
      public ResponseEntity<List<Task>> getAllTasks() {
          return ResponseEntity.ok(taskService.getAllTasks());
      }

      @GetMapping("/{id}")
      public ResponseEntity<Task> getTaskById(@PathVariable Long id) {
          return ResponseEntity.ok(taskService.getTaskById(id));
      }

      @PostMapping
      public ResponseEntity<Task> createTask(@Valid @RequestBody Task task) {
          Task created = taskService.createTask(task);
          return ResponseEntity.status(HttpStatus.CREATED).body(created);
      }

      @PutMapping("/{id}")
      public ResponseEntity<Task> updateTask(@PathVariable Long id,
                                             @Valid @RequestBody Task taskDetails) {
          return ResponseEntity.ok(taskService.updateTask(id, taskDetails));
      }

      @DeleteMapping("/{id}")
      public ResponseEntity<Void> deleteTask(@PathVariable Long id) {
          taskService.deleteTask(id);
          return ResponseEntity.noContent().build();
      }

      @GetMapping("/filter")
      public ResponseEntity<List<Task>> filterTasks(@RequestParam Boolean done) {
          return ResponseEntity.ok(taskService.filterByStatus(done));
      }
  }

  ── TaskmanagerApplication.java ──────────────────────────────────────────────

  package com.equipe.taskmanager;

  import org.springframework.boot.SpringApplication;
  import org.springframework.boot.autoconfigure.SpringBootApplication;

  @SpringBootApplication
  public class TaskmanagerApplication {
      public static void main(String[] args) {
          SpringApplication.run(TaskmanagerApplication.class, args);
      }
  }

  ── src/main/resources/application.properties ────────────────────────────────

  spring.application.name=taskmanager

  # Base de données H2 en mémoire pour développement
  spring.datasource.url=jdbc:h2:mem:taskdb
  spring.datasource.driver-class-name=org.h2.Driver
  spring.datasource.username=sa
  spring.datasource.password=

  # JPA / Hibernate
  spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
  spring.jpa.hibernate.ddl-auto=create-drop
  spring.jpa.show-sql=false

  # Console H2 (désactiver en production)
  spring.h2.console.enabled=true

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.12 - Alice crée les tests initiaux (avec JUnit 5 + Mockito + MockMvc)
────────────────────────────────────────────────────────────────────────────────

  ── test/TaskControllerTest.java ─────────────────────────────────────────────

  package com.equipe.taskmanager.controller;

  import com.equipe.taskmanager.exception.TaskNotFoundException;
  import com.equipe.taskmanager.model.Task;
  import com.equipe.taskmanager.service.TaskService;
  import com.fasterxml.jackson.databind.ObjectMapper;
  import org.junit.jupiter.api.BeforeEach;
  import org.junit.jupiter.api.Test;
  import org.springframework.beans.factory.annotation.Autowired;
  import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
  import org.springframework.boot.test.mock.mockito.MockBean;
  import org.springframework.http.MediaType;
  import org.springframework.test.web.servlet.MockMvc;
  import java.util.Arrays;
  import java.util.Collections;
  import static org.mockito.ArgumentMatchers.*;
  import static org.mockito.Mockito.*;
  import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
  import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

  @WebMvcTest(TaskController.class)
  class TaskControllerTest {

      @Autowired
      private MockMvc mockMvc;

      @Autowired
      private ObjectMapper objectMapper;

      @MockBean
      private TaskService taskService;

      private Task task1;
      private Task task2;

      @BeforeEach
      void setUp() {
          task1 = new Task(1L, "Acheter du lait", "Au supermarché", false, null);
          task2 = new Task(2L, "Aller à la gym", null, true, null);
      }

      @Test
      void testGetAllTasks_Returns200WithList() throws Exception {
          when(taskService.getAllTasks()).thenReturn(Arrays.asList(task1, task2));

          mockMvc.perform(get("/api/tasks"))
                 .andExpect(status().isOk())
                 .andExpect(jsonPath("$.length()").value(2))
                 .andExpect(jsonPath("$[0].title").value("Acheter du lait"));
      }

      @Test
      void testGetAllTasks_ReturnsEmptyList() throws Exception {
          when(taskService.getAllTasks()).thenReturn(Collections.emptyList());

          mockMvc.perform(get("/api/tasks"))
                 .andExpect(status().isOk())
                 .andExpect(jsonPath("$.length()").value(0));
      }

      @Test
      void testGetTaskById_Returns200WhenFound() throws Exception {
          when(taskService.getTaskById(1L)).thenReturn(task1);

          mockMvc.perform(get("/api/tasks/1"))
                 .andExpect(status().isOk())
                 .andExpect(jsonPath("$.id").value(1))
                 .andExpect(jsonPath("$.title").value("Acheter du lait"));
      }

      @Test
      void testGetTaskById_Returns404WhenNotFound() throws Exception {
          when(taskService.getTaskById(999L)).thenThrow(new TaskNotFoundException(999L));

          mockMvc.perform(get("/api/tasks/999"))
                 .andExpect(status().isNotFound());
      }

      @Test
      void testCreateTask_Returns201WhenValid() throws Exception {
          when(taskService.createTask(any(Task.class))).thenReturn(task1);

          mockMvc.perform(post("/api/tasks")
                         .contentType(MediaType.APPLICATION_JSON)
                         .content(objectMapper.writeValueAsString(task1)))
                 .andExpect(status().isCreated())
                 .andExpect(jsonPath("$.title").value("Acheter du lait"));
      }

      @Test
      void testCreateTask_Returns400WhenTitleMissing() throws Exception {
          Task invalidTask = new Task(null, "", null, false, null);

          mockMvc.perform(post("/api/tasks")
                         .contentType(MediaType.APPLICATION_JSON)
                         .content(objectMapper.writeValueAsString(invalidTask)))
                 .andExpect(status().isBadRequest());
      }

      @Test
      void testUpdateTask_Returns200WhenValid() throws Exception {
          when(taskService.updateTask(eq(1L), any(Task.class))).thenReturn(task2);

          mockMvc.perform(put("/api/tasks/1")
                         .contentType(MediaType.APPLICATION_JSON)
                         .content(objectMapper.writeValueAsString(task2)))
                 .andExpect(status().isOk());
      }

      @Test
      void testDeleteTask_Returns204WhenFound() throws Exception {
          doNothing().when(taskService).deleteTask(1L);

          mockMvc.perform(delete("/api/tasks/1"))
                 .andExpect(status().isNoContent());
      }

      @Test
      void testFilterTasks_Returns200() throws Exception {
          when(taskService.filterByStatus(true)).thenReturn(Arrays.asList(task2));

          mockMvc.perform(get("/api/tasks/filter?done=true"))
                 .andExpect(status().isOk())
                 .andExpect(jsonPath("$.length()").value(1));
      }
  }

  ── test/TaskServiceTest.java ─────────────────────────────────────────────────

  package com.equipe.taskmanager.service;

  import com.equipe.taskmanager.exception.TaskNotFoundException;
  import com.equipe.taskmanager.model.Task;
  import com.equipe.taskmanager.repository.TaskRepository;
  import org.junit.jupiter.api.BeforeEach;
  import org.junit.jupiter.api.Test;
  import org.junit.jupiter.api.extension.ExtendWith;
  import org.mockito.InjectMocks;
  import org.mockito.Mock;
  import org.mockito.junit.jupiter.MockitoExtension;
  import java.util.Arrays;
  import java.util.List;
  import java.util.Optional;
  import static org.assertj.core.api.Assertions.*;
  import static org.mockito.ArgumentMatchers.*;
  import static org.mockito.Mockito.*;

  @ExtendWith(MockitoExtension.class)
  class TaskServiceTest {

      @Mock
      private TaskRepository taskRepository;

      @InjectMocks
      private TaskService taskService;

      private Task task;

      @BeforeEach
      void setUp() {
          task = new Task(1L, "Ma tâche", "Description", false, null);
      }

      @Test
      void testGetAllTasks_ReturnsList() {
          when(taskRepository.findAll()).thenReturn(Arrays.asList(task));
          List<Task> tasks = taskService.getAllTasks();
          assertThat(tasks).hasSize(1);
          assertThat(tasks.get(0).getTitle()).isEqualTo("Ma tâche");
      }

      @Test
      void testGetTaskById_ReturnsTaskWhenExists() {
          when(taskRepository.findById(1L)).thenReturn(Optional.of(task));
          Task found = taskService.getTaskById(1L);
          assertThat(found.getId()).isEqualTo(1L);
      }

      @Test
      void testGetTaskById_ThrowsExceptionWhenNotFound() {
          when(taskRepository.findById(999L)).thenReturn(Optional.empty());
          assertThatThrownBy(() -> taskService.getTaskById(999L))
                  .isInstanceOf(TaskNotFoundException.class)
                  .hasMessageContaining("999");
      }

      @Test
      void testCreateTask_SavesAndReturnsTask() {
          when(taskRepository.save(any(Task.class))).thenReturn(task);
          Task created = taskService.createTask(task);
          assertThat(created.getTitle()).isEqualTo("Ma tâche");
          verify(taskRepository, times(1)).save(task);
      }

      @Test
      void testUpdateTask_UpdatesFields() {
          Task updated = new Task(1L, "Titre modifié", "Desc modifiée", true, null);
          when(taskRepository.findById(1L)).thenReturn(Optional.of(task));
          when(taskRepository.save(any(Task.class))).thenReturn(updated);

          Task result = taskService.updateTask(1L, updated);
          assertThat(result.getTitle()).isEqualTo("Titre modifié");
          assertThat(result.getDone()).isTrue();
      }

      @Test
      void testDeleteTask_DeletesWhenExists() {
          when(taskRepository.findById(1L)).thenReturn(Optional.of(task));
          doNothing().when(taskRepository).delete(task);
          taskService.deleteTask(1L);
          verify(taskRepository, times(1)).delete(task);
      }

      @Test
      void testFilterByStatus_ReturnsDoneTasks() {
          Task doneTask = new Task(2L, "Tâche faite", null, true, null);
          when(taskRepository.findByDone(true)).thenReturn(Arrays.asList(doneTask));
          List<Task> result = taskService.filterByStatus(true);
          assertThat(result).hasSize(1);
          assertThat(result.get(0).getDone()).isTrue();
      }
  }

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.13 - Alice fait la première analyse Maven + SonarQube
────────────────────────────────────────────────────────────────────────────────

  COMMANDES TERMINAL:
  ───────────────────

  # Se placer à la racine du projet (là où est le pom.xml)
  cd taskmanager-java/

  # Commande complète:
  # 1. clean     -> nettoie les anciens fichiers compilés
  # 2. verify    -> compile + teste + génère le rapport JaCoCo
  # 3. sonar:sonar -> envoie les résultats à SonarQube
  mvn clean verify sonar:sonar \
    -Dsonar.token=squ_j4v4e2c1d9b3f7a5e8c2d4f1b6e9a3c7d5f2b8e1

  SORTIE ATTENDUE (extrait):
  ──────────────────────────
  [INFO] Scanning for projects...
  [INFO] Building TaskManager Java API 1.0.0
  [INFO]
  [INFO] --- maven-clean-plugin:3.2.0:clean ---
  [INFO] Deleting .../target
  [INFO]
  [INFO] --- maven-compiler-plugin:3.11.0:compile ---
  [INFO] Compiling 6 source files
  [INFO]
  [INFO] --- maven-surefire-plugin:3.1.2:test ---
  [INFO] Running com.equipe.taskmanager.controller.TaskControllerTest
  [INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0
  [INFO] Running com.equipe.taskmanager.service.TaskServiceTest
  [INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0
  [INFO]
  [INFO] --- jacoco-maven-plugin:0.8.11:report ---
  [INFO] Loading execution data file .../target/jacoco.exec
  [INFO] Analyzed bundle 'taskmanager' with 4 classes
  [INFO]
  [INFO] --- sonar-maven-plugin:3.10.0:sonar ---
  [INFO] Load project settings
  [INFO] Load quality profiles
  [INFO] Load active rules
  [INFO] Sensor JavaSensor
  [INFO] 6 source files to be analyzed
  [INFO] Sensor JaCoCo XML Report Importer
  [INFO] Importing .../target/site/jacoco/jacoco.xml
  [INFO] Sensor JavaSensor (done) | time=12345ms
  [INFO] --------------------------------------------------------------------------
  [INFO] ANALYSIS SUCCESSFUL
  [INFO] Note that you will be able to access the report at:
  [INFO] http://localhost:9000/dashboard?id=taskmanager-java-api
  [INFO] --------------------------------------------------------------------------
  [INFO] BUILD SUCCESS
  [INFO] Total time: 28.431 s

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.14 - Alice vérifie les résultats dans l'interface
────────────────────────────────────────────────────────────────────────────────

INTERFACE WEB:

  1. Aller sur: http://localhost:9000
  2. Cliquer sur "TaskManager Java API"
  3. Premier dashboard:

  ┌────────────────────────────────────────────────────────────────────┐
  │  TaskManager Java API                                              │
  │  Last analysis: Today at 10:15  | main | 6 files | 287 lines      │
  ├──────────────────────┬────────────────────┬───────────────────────┤
  │  Reliability * A    │  Security * A     │  Maintainability * A │
  │  0 Bugs              │  0 Vulnerabilities │  1 Code Smell          │
  ├──────────────────────┴────────────────────┴───────────────────────┤
  │  Coverage: 87.3% [OK]  |  Duplications: 0.0% [OK]                    │
  ├────────────────────────────────────────────────────────────────────┤
  │  Quality Gate: [OK] PASSED                                           │
  └────────────────────────────────────────────────────────────────────┘

  Base propre! Alice partage les infos de connexion avec Bob et Claire.

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.15 - Alice génère les tokens individuels pour Bob et Claire
────────────────────────────────────────────────────────────────────────────────

INTERFACE WEB:

  Chemin: Administration > Security > Users
  Cliquer sur "bob" -> Onglet "Tokens" -> [+ Generate Token]

  Token Bob:
  ┌─────────────────────────────────────────────────────────────────┐
  │  Token name: bob-java-token                                     │
  │  Token type: User Token                                         │
  │  -> Generate                                                     │
  │  -> squ_bob_j4v4xxxxxxxxxxxxxxxxxxxxxxxxxxxx                     │
  └─────────────────────────────────────────────────────────────────┘

  Token Claire:
  ┌─────────────────────────────────────────────────────────────────┐
  │  Token name: claire-java-token                                  │
  │  Token type: User Token                                         │
  │  -> Generate                                                     │
  │  -> squ_claire_j4v4xxxxxxxxxxxxxxxxxxxxxxxxx                     │
  └─────────────────────────────────────────────────────────────────┘

  Alice envoie chaque token à la personne concernée par message privé.


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 2 - CONFIGURATION POSTE DE BOB ET CLAIRE (DÉVELOPPEURS JAVA)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 2.1 - Bob et Claire se connectent à SonarQube
────────────────────────────────────────────────────────────────────────────────

BOB sur son navigateur:

  1. Aller sur: http://sonar.equipe.local:9000
  2. Login: bob / BobPass2024!
  3. Changer le mot de passe au premier login
  4. Bob voit le dashboard du projet "TaskManager Java API"

CLAIRE fait pareil avec ses identifiants.

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 2.2 - Bob et Claire vérifient les prérequis Java/Maven
────────────────────────────────────────────────────────────────────────────────

  COMMANDES TERMINAL:
  ───────────────────

  # Vérifier Java (version 17 minimum recommandée):
  java -version
  # Attendu: openjdk version "17.x.x" ou supérieur

  # Si Java non installé:
  # Ubuntu/Debian:
  sudo apt update && sudo apt install openjdk-17-jdk

  # Mac (avec Homebrew):
  brew install openjdk@17
  echo 'export JAVA_HOME=/opt/homebrew/opt/openjdk@17' >> ~/.zshrc
  source ~/.zshrc

  # Windows: Télécharger depuis https://adoptium.net
  # Puis ajouter JAVA_HOME aux variables d'environnement

  # Vérifier Maven:
  mvn -version
  # Attendu: Apache Maven 3.9.x

  # Si Maven non installé:
  # Ubuntu:
  sudo apt install maven

  # Mac:
  brew install maven

  # Windows: Télécharger depuis https://maven.apache.org/download.cgi

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 2.3 - Bob et Claire récupèrent le projet
────────────────────────────────────────────────────────────────────────────────

  COMMANDES:
  ──────────
  # Cloner le projet
  git clone https://github.com/equipe/taskmanager-java.git
  cd taskmanager-java

  # Vérifier que le projet compile:
  mvn compile
  # Attendu: BUILD SUCCESS

  # Lancer les tests:
  mvn test
  # Attendu: Tests run: 14, Failures: 0, Errors: 0, Skipped: 0

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 2.4 - Bob et Claire configurent leur token dans les settings Maven
────────────────────────────────────────────────────────────────────────────────

La meilleure pratique Maven est de stocker le token dans settings.xml
(fichier personnel, jamais dans Git).

BOB crée/modifie son fichier settings.xml:

  COMMANDE pour voir si le fichier existe:
  ────────────────────────────────────────
  cat ~/.m2/settings.xml
  # Si le fichier n'existe pas, le créer:
  mkdir -p ~/.m2

  COMMANDE pour éditer:
  ─────────────────────
  nano ~/.m2/settings.xml

  CONTENU À CRÉER (ou à fusionner si le fichier existe déjà):
  ────────────────────────────────────────────────────────────
  <?xml version="1.0" encoding="UTF-8"?>
  <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
            http://maven.apache.org/xsd/settings-1.0.0.xsd">

    <profiles>
      <profile>
        <id>sonar</id>
        <activation>
          <activeByDefault>true</activeByDefault>
        </activation>
        <properties>
          <!-- URL du serveur SonarQube de l'équipe -->
          <sonar.host.url>http://sonar.equipe.local:9000</sonar.host.url>

          <!-- Token personnel de Bob (NE PAS mettre dans pom.xml ou Git!) -->
          <sonar.token>squ_bob_j4v4xxxxxxxxxxxxxxxxxxxxxxxxxxxx</sonar.token>
        </properties>
      </profile>
    </profiles>

  </settings>

  IMPORTANT: Ce fichier est dans ~/.m2/ (dossier personnel)
  Il n'est JAMAIS commité dans Git.
  Chaque développeur met son propre token ici.

CLAIRE fait pareil avec son propre token dans son ~/.m2/settings.xml.

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 2.5 - Bob crée le script d'analyse local
────────────────────────────────────────────────────────────────────────────────

Bob crée un script shell pratique à la racine du projet:

  COMMANDE:
  ─────────
  nano analyze.sh

  CONTENU:
  ────────
  #!/bin/bash
  # analyze.sh - Script d'analyse SonarQube pour le projet Java TaskManager

  set -e  # Arrêter si une commande échoue

  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  echo "  TaskManager Java - Analyse SonarQube"
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  echo ""
  echo "[OUTIL] Compilation + Tests + Coverage + SonarQube..."
  echo ""

  # Commande unique Maven:
  # clean   -> nettoie target/
  # verify  -> compile + test + JaCoCo report
  # sonar:sonar -> envoie à SonarQube (lit token dans ~/.m2/settings.xml)
  mvn clean verify sonar:sonar \
    -Dsonar.projectKey=taskmanager-java-api

  echo ""
  echo "[OK] Analyse terminée!"
  echo "[HAUSSE] Résultats: http://sonar.equipe.local:9000/dashboard?id=taskmanager-java-api"

  # Rendre le script exécutable:
  chmod +x analyze.sh

S'assurer que le script est dans .gitignore n'est PAS nécessaire
(ce script peut être partagé dans Git, le token est dans settings.xml).

  COMMANDE pour ajouter les fichiers Maven à .gitignore:
  ───────────────────────────────────────────────────────
  cat >> .gitignore << 'EOF'
  # Maven
  target/
  *.class

  # SonarQube
  .sonar/
  .scannerwork/

  # IDE
  .idea/
  *.iml
  .eclipse/
  .classpath
  .project
  .settings/
  EOF


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 3 - WORKFLOW QUOTIDIEN DE L'ÉQUIPE (CAS CONCRETS JAVA)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

════════════════════════════════════════════════════════════════════════════════
SCÉNARIO A - Bob ajoute une feature et introduit un bug Java typique
════════════════════════════════════════════════════════════════════════════════

Bob doit ajouter une fonctionnalité de pagination sur la liste des tâches.

──────────────────────────────────────────────────────────────────────────────
A.1 - Bob crée sa branche et écrit son code
──────────────────────────────────────────────────────────────────────────────

  COMMANDES:
  ──────────
  git checkout -b feature/pagination-tasks

Bob modifie TaskService.java en ajoutant la pagination:

  ── NOUVEAU CODE DE BOB (avec bug) ───────────────────────────────────────────

  // Dans TaskService.java - Nouveau import ajouté par Bob:
  import org.springframework.data.domain.Page;
  import org.springframework.data.domain.PageRequest;

  // Nouvelle méthode ajoutée par Bob:
  public Page<Task> getTasksPaginated(int page, int size) {

      // [X] BUG: Bob ne valide pas les paramètres!
      // Si page = -1, PageRequest.of lance une IllegalArgumentException
      // Si size = 0, division par zéro possible dans le calcul de pages
      PageRequest pageRequest = PageRequest.of(page, size);
      return taskRepository.findAll(pageRequest);
  }

Bob modifie TaskController.java:

  // [X] BUG: Pas de validation des paramètres entrants
  @GetMapping("/paginated")
  public ResponseEntity<Page<Task>> getTasksPaginated(
          @RequestParam(defaultValue = "0") int page,
          @RequestParam(defaultValue = "10") int size) {

      // [X] CODE SMELL: Variable intermédiaire inutile + NullPointerException possible
      Page<Task> result = taskService.getTasksPaginated(page, size);
      Page<Task> finalResult = result;  // Variable inutile
      if (finalResult != null) {        // Page ne peut pas être null ici
          return ResponseEntity.ok(finalResult);
      }
      return ResponseEntity.ok(null);  // [X] Retourner null dans ResponseEntity
  }

──────────────────────────────────────────────────────────────────────────────
A.2 - Bob lance l'analyse
──────────────────────────────────────────────────────────────────────────────

  COMMANDE:
  ─────────
  ./analyze.sh

  SORTIE TERMINAL (extrait):
  ──────────────────────────
  [INFO] Tests run: 14, Failures: 0, Errors: 0, Skipped: 0
  [INFO] JaCoCo coverage: 84%  (la nouvelle méthode n'est pas testée)
  [INFO] ANALYSIS SUCCESSFUL

──────────────────────────────────────────────────────────────────────────────
A.3 - Bob voit les problèmes dans SonarQube
──────────────────────────────────────────────────────────────────────────────

INTERFACE WEB:

  1. Bob va sur http://sonar.equipe.local:9000 > TaskManager Java API
  2. Il voit:

  ┌─────────────────────────────────────────────────────────────────┐
  │  Quality Gate: [X] FAILED                                        │
  │                                                                 │
  │  Reliability * B  (1 Bug)  <- avant c'était A                  │
  │  Coverage: 84%  [OK]  (>80%, ok)                                  │
  └─────────────────────────────────────────────────────────────────┘

  3. Bob clique sur "1 Bug":

  ┌──────────────────────────────────────────────────────────────────┐
  │  [BUG] Bug [MAJOR]                                                  │
  │  "ResponseEntity.ok(null)" is null-incompatible                  │
  │  TaskController.java:72                                          │
  │                                                                  │
  │  70 | if (finalResult != null) {                                 │
  │  71 |     return ResponseEntity.ok(finalResult);                 │
  │-> 72 |     return ResponseEntity.ok(null);  <- PROBLÈME           │
  │                                                                  │
  │  Why is this a bug?                                              │
  │  ResponseEntity.ok() avec null peut provoquer un               │
  │  NullPointerException lors de la sérialisation JSON.            │
  │                                                                  │
  │  Effort to fix: 5 minutes                                        │
  └──────────────────────────────────────────────────────────────────┘

  4. Bob voit aussi 2 Code Smells:

  ┌──────────────────────────────────────────────────────────────────┐
  │  [NETTOYAGE] Code Smell [MINOR]                                           │
  │  Remove this useless assignment to "finalResult"                 │
  │  TaskController.java:68                                          │
  ├──────────────────────────────────────────────────────────────────┤
  │  [NETTOYAGE] Code Smell [MINOR]                                           │
  │  This condition is always true (Page cannot be null)             │
  │  TaskController.java:70                                          │
  └──────────────────────────────────────────────────────────────────┘

──────────────────────────────────────────────────────────────────────────────
A.4 - Bob corrige son code
──────────────────────────────────────────────────────────────────────────────

Bob corrige TaskService.java:

  ── SERVICE CORRIGÉ ──────────────────────────────────────────────────────────

  public Page<Task> getTasksPaginated(int page, int size) {

      // [OK] Validation des paramètres
      if (page < 0) {
          throw new IllegalArgumentException("Le numéro de page doit être >= 0");
      }
      if (size < 1 || size > 100) {
          throw new IllegalArgumentException("La taille doit être entre 1 et 100");
      }

      PageRequest pageRequest = PageRequest.of(page, size);
      return taskRepository.findAll(pageRequest);
  }

Bob corrige TaskController.java:

  ── CONTROLLER CORRIGÉ ───────────────────────────────────────────────────────

  @GetMapping("/paginated")
  public ResponseEntity<Page<Task>> getTasksPaginated(
          @RequestParam(defaultValue = "0") int page,
          @RequestParam(defaultValue = "10") int size) {

      // [OK] Code direct, sans variable inutile
      return ResponseEntity.ok(taskService.getTasksPaginated(page, size));
  }

Bob ajoute les tests pour la nouvelle fonctionnalité:

  // Dans TaskServiceTest.java:

  @Test
  void testGetTasksPaginated_ReturnsPage() {
      Task task = new Task(1L, "Tâche", null, false, null);
      Page<Task> mockPage = new PageImpl<>(Arrays.asList(task));
      when(taskRepository.findAll(any(PageRequest.class))).thenReturn(mockPage);

      Page<Task> result = taskService.getTasksPaginated(0, 10);
      assertThat(result.getContent()).hasSize(1);
  }

  @Test
  void testGetTasksPaginated_ThrowsOnNegativePage() {
      assertThatThrownBy(() -> taskService.getTasksPaginated(-1, 10))
              .isInstanceOf(IllegalArgumentException.class)
              .hasMessageContaining(">= 0");
  }

  @Test
  void testGetTasksPaginated_ThrowsOnZeroSize() {
      assertThatThrownBy(() -> taskService.getTasksPaginated(0, 0))
              .isInstanceOf(IllegalArgumentException.class)
              .hasMessageContaining("entre 1 et 100");
  }

  // Dans TaskControllerTest.java:

  @Test
  void testGetTasksPaginated_Returns200() throws Exception {
      Page<Task> mockPage = new PageImpl<>(Arrays.asList(task1));
      when(taskService.getTasksPaginated(0, 10)).thenReturn(mockPage);

      mockMvc.perform(get("/api/tasks/paginated?page=0&size=10"))
             .andExpect(status().isOk())
             .andExpect(jsonPath("$.content.length()").value(1));
  }

──────────────────────────────────────────────────────────────────────────────
A.5 - Bob re-lance l'analyse et voit le résultat vert
──────────────────────────────────────────────────────────────────────────────

  COMMANDE:
  ─────────
  ./analyze.sh

  SORTIE:
  ───────
  [INFO] Tests run: 17, Failures: 0, Errors: 0, Skipped: 0
  [INFO] JaCoCo coverage: 89%
  [INFO] ANALYSIS SUCCESSFUL

INTERFACE WEB:

  ┌─────────────────────────────────────────────────────────────────┐
  │  TaskManager Java API                                           │
  ├─────────────────────────────────────────────────────────────────┤
  │  Quality Gate: [OK] PASSED                                        │
  │  Reliability * A  (0 Bugs)                                     │
  │  Coverage: 89% [OK]                                               │
  └─────────────────────────────────────────────────────────────────┘

Bob fait son commit:

  COMMANDES:
  ──────────
  git add src/
  git commit -m "feat: add pagination for task list

  - Add GET /api/tasks/paginated?page=0&size=10
  - Validate page (>= 0) and size (1-100) parameters
  - Add 3 service tests + 1 controller test
  - SonarQube QualityGate: PASSED (coverage 89%)"

  git push origin feature/pagination-tasks


════════════════════════════════════════════════════════════════════════════════
SCÉNARIO B - Claire introduit une vulnérabilité de sécurité Java
════════════════════════════════════════════════════════════════════════════════

Claire doit ajouter une route de recherche de tâches par mot-clé dans le titre.

──────────────────────────────────────────────────────────────────────────────
B.1 - Claire écrit le code avec une faille de sécurité
──────────────────────────────────────────────────────────────────────────────

  COMMANDES:
  ──────────
  git checkout main && git pull
  git checkout -b feature/search-tasks

Claire ajoute dans TaskService.java:

  ── CODE DE CLAIRE (avec vulnérabilité) ──────────────────────────────────────

  // Import ajouté:
  import jakarta.persistence.EntityManager;
  import jakarta.persistence.PersistenceContext;

  @PersistenceContext
  private EntityManager entityManager;

  // [X] VULNÉRABILITÉ: JPQL Injection (équivalent de SQL Injection en JPA)
  public List<Task> searchByTitle(String keyword) {
      String jpql = "SELECT t FROM Task t WHERE t.title LIKE '%" + keyword + "%'";
      return entityManager.createQuery(jpql, Task.class).getResultList();
  }

  // [X] SECURITY HOTSPOT: Gestion de mot de passe non sécurisée
  public String hashUserInput(String input) {
      // MD5 est cryptographiquement faible (détecté par SonarQube)
      try {
          java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
          byte[] hash = md.digest(input.getBytes());
          return new String(hash);
      } catch (Exception e) {
          return null;  // [X] BUG: Retourner null sur une exception
      }
  }

──────────────────────────────────────────────────────────────────────────────
B.2 - Claire lance l'analyse et voit les alertes
──────────────────────────────────────────────────────────────────────────────

  COMMANDE:
  ─────────
  ./analyze.sh

INTERFACE WEB:

  ┌─────────────────────────────────────────────────────────────────┐
  │  Quality Gate: [X] FAILED                                        │
  │                                                                 │
  │  Security * E  (1 Vulnerability BLOCKER)  <- ROUGE VIVE        │
  │  Security Review * E  (1 Hotspot non reviewé)                  │
  └─────────────────────────────────────────────────────────────────┘

Claire clique sur "1 Vulnerability":

  ┌──────────────────────────────────────────────────────────────────┐
  │  [VERROUILLE] Vulnerability [BLOCKER]                                      │
  │  "JPQL injection" - User data is used in a JPQL query           │
  │  TaskService.java:62                                             │
  │                                                                  │
  │  60 | public List<Task> searchByTitle(String keyword) {          │
  │  61 |     String jpql = "SELECT t FROM Task t "                  │
  │-> 62 |         + "WHERE t.title LIKE '%" + keyword + "%'";  [X]   │
  │  63 |     return entityManager.createQuery(jpql, Task.class)     │
  │  64 |                         .getResultList();                   │
  │                                                                  │
  │  Why is this dangerous?                                          │
  │  An attacker can inject JPQL/SQL code via 'keyword'.            │
  │  Ex: keyword = "' OR '1'='1" returns ALL tasks.                 │
  │  Ex: keyword = "'; DELETE FROM Task t WHERE '1'='1"             │
  │                                                                  │
  │  How to fix:                                                     │
  │  Use parameterized queries with named parameters.               │
  │  Use Spring Data JPA repository methods instead.                │
  │                                                                  │
  │  See also: CWE-89 | OWASP A03:2021 – Injection                  │
  └──────────────────────────────────────────────────────────────────┘

Claire clique sur "Security Hotspots":

  ┌──────────────────────────────────────────────────────────────────┐
  │  [SECURISE] Security Hotspot [HIGH]                                      │
  │  Make sure this weak hashing algorithm is not used...           │
  │  TaskService.java:71                                             │
  │                                                                  │
  │  71 | MessageDigest.getInstance("MD5")  <- FAIBLE                │
  │                                                                  │
  │  What is the risk?                                              │
  │  MD5 est cryptographiquement cassé depuis 2004.                 │
  │  Un attaquant peut retrouver le mot de passe original.          │
  │                                                                  │
  │  Ask yourself:                                                   │
  │  - Is MD5 used for password hashing? -> Use BCrypt/Argon2        │
  │  - Is MD5 used for checksums? -> Use SHA-256 minimum            │
  │                                                                  │
  │  Status: [TO REVIEW]  -> Claire doit examiner et décider        │
  └──────────────────────────────────────────────────────────────────┘

──────────────────────────────────────────────────────────────────────────────
B.3 - Claire corrige les problèmes
──────────────────────────────────────────────────────────────────────────────

Claire corrige TaskService.java:

  ── CODE CORRIGÉ DE CLAIRE ───────────────────────────────────────────────────

  // [OK] CORRECTION 1: Utiliser Spring Data JPA (déjà défini dans TaskRepository)
  // La méthode findByTitleContainingIgnoreCase() est sûre par conception
  public List<Task> searchByTitle(String keyword) {
      if (keyword == null || keyword.trim().length() < 2) {
          throw new IllegalArgumentException("Le mot-clé doit faire au moins 2 caractères");
      }
      return taskRepository.findByTitleContainingIgnoreCase(keyword.trim());
  }

  // [OK] CORRECTION 2: Supprimer la méthode hashUserInput ou la corriger
  // Si on veut vraiment hasher quelque chose, utiliser SHA-256 minimum:
  public String hashInput(String input) {
      if (input == null) {
          throw new IllegalArgumentException("L'entrée ne peut pas être null");
      }
      try {
          java.security.MessageDigest sha256 =
              java.security.MessageDigest.getInstance("SHA-256");  // [OK] SHA-256
          byte[] hash = sha256.digest(input.getBytes(java.nio.charset.StandardCharsets.UTF_8));
          return java.util.Base64.getEncoder().encodeToString(hash);
      } catch (java.security.NoSuchAlgorithmException e) {
          // [OK] Lancer une exception explicite plutôt que retourner null
          throw new RuntimeException("Algorithme SHA-256 non disponible", e);
      }
  }

Claire gère le Security Hotspot dans l'interface SonarQube:

INTERFACE WEB:

  1. Claire va dans Security Hotspots
  2. Elle clique sur le hotspot MD5
  3. Elle change son code pour utiliser SHA-256 (fait ci-dessus)
  4. Elle marque le hotspot:
     ┌────────────────────────────────────────────────────────────┐
     │  Security Hotspot: Weak hashing algorithm                  │
     │                                                            │
     │  Status: [TO REVIEW [BLACK_DOWN-POINTING_TRIANGLE]]  -> Claire sélectionne "Fixed"      │
     │                                                            │
     │  Comment:                                                  │
     │  Remplacé MD5 par SHA-256. Confirmé que ce hash n'est     │
     │  pas utilisé pour des mots de passe (aucun mot de passe   │
     │  dans ce projet). Utilisé pour checksums non-critiques.   │
     │                                                            │
     │  [  Save  ]                                                │
     └────────────────────────────────────────────────────────────┘

Claire ajoute les tests:

  // Dans TaskServiceTest.java:

  @Test
  void testSearchByTitle_ReturnsMatchingTasks() {
      Task task = new Task(1L, "Acheter du lait", null, false, null);
      when(taskRepository.findByTitleContainingIgnoreCase("lait"))
              .thenReturn(Arrays.asList(task));

      List<Task> result = taskService.searchByTitle("lait");
      assertThat(result).hasSize(1);
      assertThat(result.get(0).getTitle()).contains("lait");
  }

  @Test
  void testSearchByTitle_ThrowsWhenKeywordTooShort() {
      assertThatThrownBy(() -> taskService.searchByTitle("a"))
              .isInstanceOf(IllegalArgumentException.class);
  }

  @Test
  void testSearchByTitle_ThrowsWhenKeywordNull() {
      assertThatThrownBy(() -> taskService.searchByTitle(null))
              .isInstanceOf(IllegalArgumentException.class);
  }

──────────────────────────────────────────────────────────────────────────────
B.4 - Claire re-lance et vérifie
──────────────────────────────────────────────────────────────────────────────

  COMMANDE:
  ─────────
  ./analyze.sh

INTERFACE WEB:

  ┌─────────────────────────────────────────────────────────────────┐
  │  Quality Gate: [OK] PASSED                                        │
  │                                                                 │
  │  Security * A  (0 Vulnerabilities)                             │
  │  Security Review * A  (100% hotspots reviewed)                 │
  │  Coverage: 91% [OK]                                               │
  └─────────────────────────────────────────────────────────────────┘


════════════════════════════════════════════════════════════════════════════════
SCÉNARIO C - Alice fait la revue hebdomadaire Java dans SonarQube
════════════════════════════════════════════════════════════════════════════════

──────────────────────────────────────────────────────────────────────────────
C.1 - Alice consulte la vue de couverture par fichier
──────────────────────────────────────────────────────────────────────────────

INTERFACE WEB:

  1. Alice va sur: TaskManager Java API > Code
  2. Elle voit l'arborescence des fichiers avec couverture:

  ┌──────────────────────────────────────────────────────────────────┐
  │  Code                                                            │
  │                                                                  │
  │  [DOSSIER] com/equipe/taskmanager/                                      │
  │  ├── [DOSSIER] controller/                                              │
  │  │   └── [FICHIER] TaskController.java    Coverage: 78%  [ATTENTION]            │
  │  ├── [DOSSIER] service/                                                 │
  │  │   └── [FICHIER] TaskService.java       Coverage: 94%  [OK]            │
  │  ├── [DOSSIER] repository/                                              │
  │  │   └── [FICHIER] TaskRepository.java    Coverage: N/A (interface)    │
  │  ├── [DOSSIER] model/                                                   │
  │  │   └── [FICHIER] Task.java              Excluded                      │
  │  └── [DOSSIER] exception/                                               │
  │      └── [FICHIER] TaskNotFoundException.java  Excluded                 │
  └──────────────────────────────────────────────────────────────────┘

  3. Alice clique sur TaskController.java -> Coverage: 78%

  4. Elle voit le code coloré:
     Lignes VERTES = couvertes par les tests
     Lignes ROUGES = non couvertes

     34 |  @GetMapping("/paginated")                           <- VERT [OK]
     35 |  public ResponseEntity<Page<Task>> getTasksPaginated(  VERT [OK]
     36 |    @RequestParam(defaultValue = "0") int page,      <- VERT [OK]
     37 |    @RequestParam(defaultValue = "10") int size) {   <- VERT [OK]
     38 |    return ResponseEntity.ok(                         <- VERT [OK]
     39 |      taskService.getTasksPaginated(page, size));    <- VERT [OK]
     40 |  }
     41 |
     42 |  @GetMapping("/search")                             <- ROUGE [X]
     43 |  public ResponseEntity<List<Task>> searchTasks(     <- ROUGE [X]
     44 |    @RequestParam String keyword) {                  <- ROUGE [X]
     45 |    return ResponseEntity.ok(                        <- ROUGE [X]
     46 |      taskService.searchByTitle(keyword));           <- ROUGE [X]
     47 |  }

  -> Manque les tests du contrôleur pour la route /search!

──────────────────────────────────────────────────────────────────────────────
C.2 - Alice assigne les issues dans l'interface
──────────────────────────────────────────────────────────────────────────────

INTERFACE WEB:

  1. Alice va dans Issues > Ouverts
  2. Elle voit 4 Code Smells non assignés
  3. Elle sélectionne les 3 Code Smells dans le controller:
     [WHITE_SQUARE] Cliquer la checkbox de chaque issue
     -> Apparaît le bouton "Bulk Change" en haut

  4. Elle clique "Bulk Change":
     ┌────────────────────────────────────────────────────────────┐
     │  Bulk Change (3 issues sélectionnées)                      │
     │                                                            │
     │  Assign to: [bob    [BLACK_DOWN-POINTING_TRIANGLE]]                                     │
     │  Comment:   [Ces 3 code smells sont dans les nouveaux     │
     │              controllers que tu as écrits. À corriger     │
     │              dans ta prochaine PR s'il te plaît. -Alice]  │
     │                                                            │
     │  [  Apply  ]                                               │
     └────────────────────────────────────────────────────────────┘

  5. Pour le Code Smell dans le service (méthode trop longue):
     Assign to: claire
     Comment: "La méthode updateTask peut être refactorisée. -Alice"

──────────────────────────────────────────────────────────────────────────────
C.3 - Alice consulte les métriques d'évolution dans "Activity"
──────────────────────────────────────────────────────────────────────────────

INTERFACE WEB:

  1. TaskManager Java API > Activity

  2. Alice sélectionne "Coverage" dans le graphique de métriques:
     ┌──────────────────────────────────────────────────────────────┐
     │  Coverage over time                                          │
     │                                                              │
     │  100% ─                                                      │
     │   90% ─         [BLACK_CIRCLE]─────[BLACK_CIRCLE]─────[BLACK_CIRCLE]                               │
     │   80% ─   [BLACK_CIRCLE]─────/                                           │
     │   70% ─                                                      │
     │   60% ─                                                      │
     │        ────────────────────────────────────────────────────  │
     │        Sem 1   Sem 2   Sem 3   Sem 4                        │
     │                                                              │
     │  Tendance: ^ 87% -> 89% -> 91% [OK]                            │
     └──────────────────────────────────────────────────────────────┘

  3. Alice sélectionne "Technical Debt":
     ┌──────────────────────────────────────────────────────────────┐
     │  Technical Debt                                              │
     │                                                              │
     │  Sem 1: 45min   Sem 2: 38min   Sem 3: 28min   Sem 4: 22min  │
     │  Tendance: v Bonne progression!                             │
     └──────────────────────────────────────────────────────────────┘

  4. Alice envoie un message à l'équipe dans le chat:
     "Bravo à tous! Coverage +4% cette semaine, dette technique -50% ce mois.
      Continuez comme ça! N'oubliez pas de corriger vos issues assignées."


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 4 - INTÉGRATION CONTINUE AVEC GITHUB ACTIONS (JAVA / MAVEN)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 4.1 - Alice configure les secrets GitHub
────────────────────────────────────────────────────────────────────────────────

INTERFACE GITHUB:

  Chemin: github.com/equipe/taskmanager-java > Settings > Secrets > Actions

  Secret 1:
  ┌─────────────────────────────────────────────────────────────────┐
  │  Name:   SONAR_TOKEN                                            │
  │  Value:  squ_j4v4e2c1d9b3f7a5e8c2d4f1b6e9a3c7d5f2b8e1        │
  │  -> [Add secret]                                                 │
  └─────────────────────────────────────────────────────────────────┘

  Secret 2:
  ┌─────────────────────────────────────────────────────────────────┐
  │  Name:   SONAR_HOST_URL                                         │
  │  Value:  http://sonar.equipe.local:9000                         │
  │  -> [Add secret]                                                 │
  └─────────────────────────────────────────────────────────────────┘

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 4.2 - Alice crée le fichier GitHub Actions pour Java/Maven
────────────────────────────────────────────────────────────────────────────────

  COMMANDE:
  ─────────
  mkdir -p .github/workflows
  nano .github/workflows/sonarqube.yml

  CONTENU COMPLET:
  ────────────────

  name: SonarQube - Analyse Qualité Java

  on:
    push:
      branches:
        - main
        - develop
        - 'feature/**'
    pull_request:
      branches:
        - main

  jobs:
    sonarqube-analysis:
      name: Build + Tests + SonarQube
      runs-on: ubuntu-latest

      steps:
        # Étape 1: Récupérer le code avec tout l'historique Git
        - name: Checkout code
          uses: actions/checkout@v4
          with:
            fetch-depth: 0    # OBLIGATOIRE pour SonarQube (analyse de branches)

        # Étape 2: Configurer Java 17 (Temurin = Eclipse Adoptium, recommandé)
        - name: Setup Java 17
          uses: actions/setup-java@v4
          with:
            java-version: '17'
            distribution: 'temurin'

        # Étape 3: Cache Maven (accélère les builds en évitant re-téléchargement)
        - name: Cache Maven packages
          uses: actions/cache@v3
          with:
            path: ~/.m2/repository
            key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
            restore-keys: |
              ${{ runner.os }}-maven-

        # Étape 4: Cache SonarQube (accélère les analyses répétées)
        - name: Cache SonarQube packages
          uses: actions/cache@v3
          with:
            path: ~/.sonar/cache
            key: ${{ runner.os }}-sonar
            restore-keys: ${{ runner.os }}-sonar

        # Étape 5: Compiler + Tester + Coverage JaCoCo + Analyser avec SonarQube
        # TOUT EN UNE SEULE COMMANDE MAVEN (plus efficace)
        - name: Build, Test & SonarQube Analysis
          env:
            GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}   # Pour PR decoration
            SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
            SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
          run: |
            mvn clean verify sonar:sonar \
              -Dsonar.projectKey=taskmanager-java-api \
              -Dsonar.host.url=$SONAR_HOST_URL \
              -Dsonar.token=$SONAR_TOKEN

        # Étape 6: Vérifier la Quality Gate (bloque le merge si FAILED)
        - name: SonarQube Quality Gate Check
          uses: sonarsource/sonarqube-quality-gate-action@master
          timeout-minutes: 5
          env:
            SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
          with:
            scanMetadataReportFile: target/sonar/report-task.txt

        # Étape 7 (optionnel): Publier le rapport de couverture en artifact
        - name: Upload coverage report
          uses: actions/upload-artifact@v3
          if: always()
          with:
            name: coverage-report
            path: target/site/jacoco/
            retention-days: 7

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 4.3 - Alice commite et teste la pipeline
────────────────────────────────────────────────────────────────────────────────

  COMMANDES:
  ──────────
  git add .github/workflows/sonarqube.yml
  git commit -m "ci: add SonarQube GitHub Actions pipeline for Java/Maven"
  git push origin main

INTERFACE GITHUB - Actions:

  [OK] Build, Test & SonarQube Analysis    (35s)
  [OK] SonarQube Quality Gate Check        (8s)
  [OK] Upload coverage report              (3s)

  Durée totale: ~3 min

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 4.4 - Ce que voit l'équipe dans une Pull Request
────────────────────────────────────────────────────────────────────────────────

Quand Bob pousse sa PR feature/pagination-tasks:

INTERFACE GITHUB - Pull Request de Bob:

  ┌──────────────────────────────────────────────────────────────────┐
  │  feat: add pagination for task list                              │
  │                                                                  │
  │  Checks                                                          │
  │  [OK] Build, Test & SonarQube Analysis — All checks passed        │
  │     Coverage: 89% | 0 Bugs | 0 Vulnerabilities                  │
  │     View full report: http://sonar.equipe.local:9000/...        │
  │  [OK] SonarQube Quality Gate Check — PASSED                       │
  │                                                                  │
  │  This branch has no conflicts with the base branch              │
  │                                                                  │
  │  [  [OK] Merge pull request  ]  <- Bouton actif, Alice peut merger │
  └──────────────────────────────────────────────────────────────────┘

  Si la QualityGate échoue:
  ┌──────────────────────────────────────────────────────────────────┐
  │  [X] SonarQube Quality Gate Check — FAILED                        │
  │     New Coverage: 72% (required: 80%)                           │
  │     New Bugs: 1                                                  │
  │                                                                  │
  │  Required check has not passed                                   │
  │  [  Merge pull request  ]  <- GRISÉ, impossible de merger!      │
  └──────────────────────────────────────────────────────────────────┘


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 5 - SPÉCIFICITÉS JAVA À CONNAÎTRE POUR SONARQUBE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

────────────────────────────────────────────────────────────────────────────────
5.1 - Comment JaCoCo et SonarQube fonctionnent ensemble
────────────────────────────────────────────────────────────────────────────────

  Schéma du flux:
  ─────────────────

  mvn clean verify sonar:sonar
         │
         ├── mvn clean -> supprime target/
         │
         ├── mvn compile -> compile les .java -> .class dans target/classes/
         │
         ├── mvn test
         │     │
         │     ├── JaCoCo "prepare-agent" -> instrumente les .class
         │     │   (ajoute du code pour tracer ce qui est exécuté)
         │     │
         │     ├── JUnit exécute les tests
         │     │
         │     └── JaCoCo génère:
         │           target/jacoco.exec     (données brutes)
         │           target/site/jacoco/jacoco.xml  (rapport XML pour SonarQube)
         │           target/site/jacoco/index.html  (rapport HTML lisible)
         │
         └── mvn sonar:sonar
               │
               ├── Lit jacoco.xml
               ├── Lit les fichiers .java et .class
               ├── Envoie tout au serveur SonarQube
               └── SonarQube calcule métriques et affiche le dashboard

────────────────────────────────────────────────────────────────────────────────
5.2 - Types de tests Java et leur impact sur le coverage
────────────────────────────────────────────────────────────────────────────────

  TYPE 1 - Tests unitaires (@ExtendWith(MockitoExtension.class))
  ──────────────────────────────────────────────────────────────
  Testent une classe isolée avec des mocks.

  @ExtendWith(MockitoExtension.class)
  class TaskServiceTest {
      @Mock TaskRepository taskRepository;   // Simulé
      @InjectMocks TaskService taskService;  // Testé
      ...
  }

  [OK] Rapides (millisecondes)
  [OK] Couvrent la logique métier (service, utils)
  [OK] Pas besoin de Spring démarré

  TYPE 2 - Tests de contrôleur (@WebMvcTest)
  ───────────────────────────────────────────
  Testent seulement la couche web avec MockMvc.

  @WebMvcTest(TaskController.class)
  class TaskControllerTest {
      @Autowired MockMvc mockMvc;       // Simule les requêtes HTTP
      @MockBean TaskService taskService; // Service simulé
      ...
  }

  [OK] Test complet de la couche HTTP (routes, codes de statut, JSON)
  [OK] Rapides (Spring web context partiel)
  [OK] Couvrent le controller

  TYPE 3 - Tests d'intégration (@SpringBootTest)
  ───────────────────────────────────────────────
  Testent l'application complète avec une vraie base de données (H2).

  @SpringBootTest
  @AutoConfigureMockMvc
  class TaskIntegrationTest {
      @Autowired MockMvc mockMvc;   // Vraie application Spring
      // Pas de Mock: tout est réel (service + repo + H2)
      ...
  }

  [OK] Test end-to-end
  [ATTENTION] Plus lents (démarrage Spring complet)
  [OK] Maximum de coverage réaliste

────────────────────────────────────────────────────────────────────────────────
5.3 - Règles SonarQube spécifiques à Java (les plus fréquentes)
────────────────────────────────────────────────────────────────────────────────

  RÈGLE 1 - NullPointerException potentielle
  ─────────────────────────────────────────
  [X] Code problématique:
  String name = task.getTitle().toUpperCase();  // NPE si title est null

  [OK] Code corrigé:
  String name = Optional.ofNullable(task.getTitle())
                        .map(String::toUpperCase)
                        .orElse("");

  RÈGLE 2 - Ressources non fermées
  ─────────────────────────────────
  [X] Code problématique:
  FileInputStream fis = new FileInputStream("file.txt");
  // ... lecture du fichier
  // [X] Si exception, fis n'est jamais fermé (resource leak)

  [OK] Code corrigé:
  try (FileInputStream fis = new FileInputStream("file.txt")) {
      // ... lecture du fichier
  }  // [OK] Fermé automatiquement (try-with-resources)

  RÈGLE 3 - Exception swallowed (avalée)
  ───────────────────────────────────────
  [X] Code problématique:
  try {
      riskyOperation();
  } catch (Exception e) {
      // [X] Exception ignorée silencieusement!
  }

  [OK] Code corrigé:
  try {
      riskyOperation();
  } catch (Exception e) {
      log.error("Erreur dans riskyOperation", e);  // [OK] Loggée
      throw new ServiceException("Opération échouée", e);  // [OK] Ou re-lancée
  }

  RÈGLE 4 - String comparison avec ==
  ─────────────────────────────────────
  [X] Code problématique:
  if (status == "done") { ... }  // [X] Compare les références, pas le contenu

  [OK] Code corrigé:
  if ("done".equals(status)) { ... }  // [OK] "done" en premier évite NPE

  RÈGLE 5 - Complexity cyclomatique trop haute
  ─────────────────────────────────────────────
  [X] Code problématique (>10 branches):
  public String processTask(Task task) {
      if (task == null) { ... }
      if (task.getTitle() == null) { ... }
      if (task.getDone() == null) { ... }
      if (task.getDone()) {
          if (task.getDescription() != null) { ... }
          else { ... }
      } else {
          if (task.getDescription() != null) { ... }
          else { ... }
      }
      // ... etc
  }

  [OK] Code corrigé (méthodes extraites):
  public String processTask(Task task) {
      validateTask(task);
      return task.getDone() ? processDoneTask(task) : processPendingTask(task);
  }

  private void validateTask(Task task) { ... }
  private String processDoneTask(Task task) { ... }
  private String processPendingTask(Task task) { ... }

────────────────────────────────────────────────────────────────────────────────
5.4 - Voir le rapport JaCoCo en HTML (localement)
────────────────────────────────────────────────────────────────────────────────

Après mvn verify, Bob peut voir exactement quelles lignes sont couvertes:

  COMMANDES:
  ──────────
  # Générer seulement les tests + couverture (sans SonarQube):
  mvn clean test

  # Ouvrir le rapport HTML dans le navigateur:
  # Mac:
  open target/site/jacoco/index.html

  # Linux:
  xdg-open target/site/jacoco/index.html

  # Windows:
  start target/site/jacoco/index.html

  VUE DU RAPPORT HTML:
  ────────────────────
  ┌──────────────────────────────────────────────────────────────────┐
  │  JaCoCo Coverage Report - TaskManager                           │
  ├─────────────────────────┬────────┬────────┬─────────────────────┤
  │  Package                │ Lines  │ Missed │ Coverage            │
  ├─────────────────────────┼────────┼────────┼─────────────────────┤
  │  controller             │  45    │  9     │  80%  [JAUNE]            │
  │  service                │  62    │  4     │  94%  [VERT]            │
  │  repository             │  -     │  -     │  N/A (interface)    │
  ├─────────────────────────┼────────┼────────┼─────────────────────┤
  │  Total                  │  107   │  13    │  88%  [VERT]            │
  └─────────────────────────┴────────┴────────┴─────────────────────┘

  En cliquant sur TaskController.java:
  Lignes VERTES = couvertes
  Lignes ROUGES = non couvertes
  Lignes JAUNES = partiellement couvertes (if sans else testé)

────────────────────────────────────────────────────────────────────────────────
5.5 - Exclure des classes du coverage (cas légitimes)
────────────────────────────────────────────────────────────────────────────────

Certaines classes ne nécessitent pas de tests directs:

  Dans pom.xml (sonar.coverage.exclusions déjà vu en 1.10):
  ──────────────────────────────────────────────────────────
  <sonar.coverage.exclusions>
    **/TaskmanagerApplication.java,   <- Point d'entrée Spring Boot
    **/model/**,                       <- POJOs / Entités JPA
    **/exception/**,                   <- Exceptions simples
    **/config/**,                      <- Fichiers de configuration Spring
    **/*DTO.java,                      <- Data Transfer Objects simples
    **/*Request.java,                  <- Objets de requête
    **/*Response.java                  <- Objets de réponse
  </sonar.coverage.exclusions>

  Pourquoi exclure ces fichiers?
  - TaskmanagerApplication.java : Testé indirectement par tous les @SpringBootTest
  - model/** : Ce sont des entités JPA sans logique (Lombok génère getters/setters)
  - exception/** : Héritent de RuntimeException, pas de logique propre
  - config/** : Testés par les tests d'intégration Spring


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 6 - BONNES PRATIQUES JAVA + SONARQUBE DE L'ÉQUIPE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

────────────────────────────────────────────────────────────────────────────────
RÈGLE 1 - La commande maven complète avant chaque push
────────────────────────────────────────────────────────────────────────────────

  # [OK] Workflow correct:
  ./analyze.sh          # Équivaut à: mvn clean verify sonar:sonar
  git add .
  git commit -m "..."
  git push

  # [X] Workflow interdit:
  mvn compile            # Juste compiler, sans tests
  git push               # Push sans avoir vérifié la qualité

────────────────────────────────────────────────────────────────────────────────
RÈGLE 2 - Couverture par couche
────────────────────────────────────────────────────────────────────────────────

  Layer        | Type de test requis          | Coverage cible
  ─────────────┼──────────────────────────────┼───────────────
  Controller   | @WebMvcTest + MockMvc         | > 80%
  Service      | @ExtendWith(MockitoExtension) | > 90%
  Repository   | @DataJpaTest ou exclure       | N/A (interface)
  Model        | Exclure (Lombok)              | N/A

────────────────────────────────────────────────────────────────────────────────
RÈGLE 3 - Conventions de nommage des tests
────────────────────────────────────────────────────────────────────────────────

  Convention de l'équipe:
  test[MethodName]_[Scenario]_[ExpectedResult]

  Exemples:
  testGetTaskById_WhenExists_Returns200()
  testGetTaskById_WhenNotFound_Returns404()
  testCreateTask_WhenTitleMissing_Returns400()
  testDeleteTask_WhenExists_DeletesAndReturns204()
  testFilterTasks_WhenDoneTrue_ReturnsOnlyDoneTasks()

────────────────────────────────────────────────────────────────────────────────
RÈGLE 4 - Traiter les Security Hotspots dans la journée
────────────────────────────────────────────────────────────────────────────────

  Quand un Security Hotspot apparaît dans SonarQube:

  1. Lire la description dans SonarQube (5 min)
  2. Évaluer: est-ce un vrai risque dans NOTRE contexte?
     - OUI -> Corriger le code, marquer "Fixed"
     - NON -> Expliquer pourquoi dans le commentaire, marquer "Safe"
  3. JAMAIS ignorer silencieusement (ne pas fermer sans commentaire)

  Format de commentaire pour "Safe":
  "Ce code utilise MD5 uniquement pour un checksum non-critique de nom de fichier,
   pas pour des mots de passe ou données sensibles. Risque acceptable ici."

────────────────────────────────────────────────────────────────────────────────
RÈGLE 5 - Utiliser @SuppressWarnings avec parcimonie
────────────────────────────────────────────────────────────────────────────────

  En Java, l'équivalent de NOSONAR est @SuppressWarnings ou le commentaire NOSONAR.

  # [X] Usage interdit (contournement paresseux):
  @SuppressWarnings("java:S2184")  // Supprime une règle sans justification

  # [OK] Usage acceptable (vrai faux positif documenté):
  // NOSONAR - SonarQube détecte une vulnérabilité dans ce wrapper de test
  // Ce code n'est utilisé que dans les tests, jamais en production
  @SuppressWarnings("java:S5976")


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 7 - RÉSUMÉ VISUEL ET COMMANDES DE RÉFÉRENCE JAVA
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

────────────────────────────────────────────────────────────────────────────────
QUI FAIT QUOI ET QUAND
────────────────────────────────────────────────────────────────────────────────

  ALICE (Lead / Admin):
  [OK] Configure SonarQube, projet, Quality Gate (une fois)
  [OK] Crée comptes Bob et Claire (une fois)
  [OK] Configure pom.xml avec JaCoCo et sonar-maven-plugin (une fois)
  [OK] Configure GitHub Actions (une fois)
  [OK] Revue hebdomadaire + assignation des issues
  [OK] Valide et merge les PRs si Quality Gate PASSED

  BOB et CLAIRE (Développeurs):
  [OK] ./analyze.sh avant chaque git push
  [OK] Tests pour chaque nouvelle méthode publique
  [OK] Correction des issues assignées
  [OK] Traitement des Security Hotspots dans la journée

────────────────────────────────────────────────────────────────────────────────
WORKFLOW COMPLET D'UNE FEATURE JAVA
────────────────────────────────────────────────────────────────────────────────

  Bob                    Local                  SonarQube         GitHub          Alice
  ───                    ─────                  ─────────         ──────          ─────
  git checkout -b feat
  [écrire code Java]
  [écrire tests JUnit]
  ./analyze.sh ──────-> mvn clean verify ──-> JaCoCo .xml ──-> Dashboard
                       sonar:sonar                          [voir métriques]
  [corriger si FAILED]
  git push ────────────────────────────────────────────────────────-> [pipeline CI]
                                                            mvn clean verify sonar:sonar
                                                            QG check ────────────-> [voir PR]
                                                                                   [merger [OK]]
                                                                                   [ask fix [X]]

────────────────────────────────────────────────────────────────────────────────
COMMANDES DE RÉFÉRENCE RAPIDE (CHEATSHEET JAVA)
────────────────────────────────────────────────────────────────────────────────

  COMMANDES DU QUOTIDIEN:
  ───────────────────────

  # Analyse complète (compile + tests + coverage + SonarQube):
  ./analyze.sh
  # Équivaut à:
  mvn clean verify sonar:sonar -Dsonar.projectKey=taskmanager-java-api

  # Seulement compiler (sans tests):
  mvn compile

  # Seulement lancer les tests:
  mvn test

  # Tests + générer rapport JaCoCo (sans SonarQube):
  mvn clean verify
  # Rapport disponible dans: target/site/jacoco/index.html

  # Voir les tests en détail (verbeux):
  mvn test -Dsurefire.useFile=false

  # Lancer UN seul test spécifique:
  mvn test -Dtest=TaskServiceTest
  mvn test -Dtest=TaskServiceTest#testGetTaskById_ReturnsTaskWhenExists

  # Lancer UN seul fichier de test:
  mvn test -Dtest=TaskControllerTest

  # Ignorer les tests (seulement compiler):
  mvn clean install -DskipTests

  # Ignorer JaCoCo check temporairement (pour déboguer):
  mvn clean verify -Djacoco.skip=true

  # Voir le rapport JaCoCo dans le navigateur:
  open target/site/jacoco/index.html    # Mac
  xdg-open target/site/jacoco/index.html  # Linux

  MAVEN - COMMANDES UTILES:
  ─────────────────────────

  # Voir les dépendances du projet:
  mvn dependency:tree

  # Vérifier si des mises à jour de dépendances sont disponibles:
  mvn versions:display-dependency-updates

  # Nettoyer le cache local Maven (si problèmes):
  mvn dependency:purge-local-repository

  SONARQUBE - COMMANDES SPÉCIFIQUES:
  ───────────────────────────────────

  # Analyse avec debug (logs très détaillés):
  mvn clean verify sonar:sonar -X -Dsonar.projectKey=taskmanager-java-api

  # Analyse en spécifiant explicitement les options:
  mvn clean verify sonar:sonar \
    -Dsonar.projectKey=taskmanager-java-api \
    -Dsonar.host.url=http://sonar.equipe.local:9000 \
    -Dsonar.token=squ_votre_token

  # Ignorer les failures JaCoCo pour tester SonarQube:
  mvn clean verify sonar:sonar \
    -Djacoco.skip.check=true \
    -Dsonar.projectKey=taskmanager-java-api

  URLS UTILES:
  ────────────

  # Dashboard principal:
  http://sonar.equipe.local:9000/dashboard?id=taskmanager-java-api

  # Issues ouvertes:
  http://sonar.equipe.local:9000/project/issues?id=taskmanager-java-api&resolved=false

  # Mes issues assignées (remplacer "bob" par son login):
  http://sonar.equipe.local:9000/issues?assignees=bob

  # Coverage par fichier:
  http://sonar.equipe.local:9000/component_measures?id=taskmanager-java-api&metric=coverage

  # Activité / Historique:
  http://sonar.equipe.local:9000/project/activity?id=taskmanager-java-api

  # Security Hotspots:
  http://sonar.equipe.local:9000/security_hotspots?id=taskmanager-java-api

  ALIAS PRATIQUES (~/.bashrc ou ~/.zshrc):
  ─────────────────────────────────────────

  alias sq='./analyze.sh'
  alias sqtest='mvn clean test'
  alias sqreport='mvn clean verify && open target/site/jacoco/index.html'
  alias sqopen='open http://sonar.equipe.local:9000/dashboard?id=taskmanager-java-api'

  # Utilisation:
  sq         # Analyse complète
  sqtest     # Tests seulement
  sqreport   # Tests + ouvre rapport coverage HTML
  sqopen     # Ouvre le dashboard SonarQube

  COMPARAISON PYTHON (FLASK) VS JAVA (SPRING BOOT):
  ──────────────────────────────────────────────────

  Aspect           │ Python/Flask          │ Java/Spring Boot
  ─────────────────┼───────────────────────┼──────────────────────────
  Coverage tool    │ pytest-cov            │ JaCoCo (plugin Maven)
  Coverage format  │ coverage.xml          │ target/site/jacoco/jacoco.xml
  Analyse command  │ sonar-scanner         │ mvn sonar:sonar
  Config           │ sonar-project.props   │ pom.xml <properties>
  Token storage    │ .env local            │ ~/.m2/settings.xml
  CI/CD command    │ sonar-scanner -D...   │ mvn clean verify sonar:sonar -D...
  Tests            │ pytest + conftest.py  │ JUnit 5 + Mockito + MockMvc

================================================================================
FIN DE LA SECTION - SONARQUBE EN ÉQUIPE DE 3 DÉVELOPPEURS (JAVA / SPRING BOOT)
================================================================================