Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 1320 minIntermédiaire

Épreuve blanche E6 n° 1 : renvoyer une note de frais

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • t'entraîner sur une épreuve E6 complète ;
  • comparer ton analyse et ton code à une correction ;
  • mesurer ce qu'il te reste à travailler.

4 exercices corrigés

Cette épreuve blanche de l'E6 SLAM reproduit une épreuve complète sur une application web de notes de frais : l'expression des besoins, la préparation, l'entretien de phase 1, l'ajustement de la commission, la réalisation, puis la recette. Passe-la chronomètre en main, avec un camarade ou un professeur dans le rôle de la commission, et n'ouvre chaque réponse qu'après avoir produit la tienne.

Comment l'utiliser

La réalisation retenue est Sorbier Frais, l'application de notes de frais décrite dans le chapitre les évolutions types. Si ta réalisation ne ressemble pas à celle-ci, transpose le sujet : remplace « note de frais » par un objet de ton application qui change d'état (un ticket, une commande, une réservation) et « comptable » par le rôle qui contrôle. La méthode et les questions restent les mêmes.

TempsDuréeCe que tu produis
Préparation de la phase 130 minta feuille : reformulation, implicite, spécifications, outils, plan
Entretien de la phase 120 minton exposé, puis tes réponses aux questions
Préparation de la phase 21 hle code, les tests, le rapport de test, l'historique Git
Recette et entretien20 minta présentation et tes réponses

Ce que la commission connaît de ta réalisation

D'après ta fiche descriptive et ton dépôt : une API REST Node.js et Express 5, un front React, une base MySQL 8.4 avec les tables service, utilisateur, note_frais et ligne_frais. Une note passe par quatre états : saisie, cloturee, validee, remboursee. Les routes existantes :

RouteRôleEffet
GET /api/notessalarié ou comptableses notes pour un salarié, toutes pour un comptable
GET /api/notes/:idpropriétaire ou comptablele détail d'une note
PATCH /api/notes/:id/cloturesalarié propriétairesaisie vers cloturee, date_cloture renseignée
PATCH /api/notes/:id/validationcomptablecloturee vers validee, montant_valide renseigné

Les middlewares requireAuth et requireRole existent, ainsi que 31 tests Vitest et supertest.

L'expression des besoins

Réalisation professionnelle retenue : n° 2, Sorbier Frais.

Circonstances de l'expression des besoins. Chaque mois, le service comptable contrôle les notes clôturées. Quand une note contient une erreur (ticket illisible, kilométrage incohérent), la comptable appelle le salarié, qui ne peut plus rien modifier puisque la note est clôturée. Elle corrige alors elle-même, sans trace. Le responsable administratif veut que tout passe par l'application.

Spécifications fonctionnelles de la production attendue.

  1. Depuis le contrôle d'une note clôturée, un comptable peut la renvoyer à son auteur en saisissant un motif obligatoire, de 10 à 500 caractères.
  2. Une note renvoyée repasse à l'état « saisie » : le salarié peut la corriger, puis la clôturer à nouveau.
  3. Le salarié voit le motif du dernier renvoi de sa note.
  4. Seul un comptable peut renvoyer une note, et seulement une note clôturée.

Documents fournis. Le MLD actuel, la liste des routes existantes.

Production attendue. L'évolution de la base de données et de l'API. L'affichage du motif dans le front si le temps le permet.

Documentation à présenter en appui de la solution. Rapport de test.

Au cours de l'épreuve, tu peux utiliser les ressources électroniques du centre d'examen, à l'exception de tout service d'échange avec un tiers. Tes connexions peuvent être contrôlées.

Phase 1 : la préparation

Exercice 1 sur 4 Ta feuille de préparation (30 minutes) Réussi À revoir

Produis, en 30 minutes et sans coder :

  1. la reformulation en une phrase ;
  2. les quatre questions : quoi, où, quel impact, quelles compétences ;
  3. la grille des six axes, avec au moins deux implicites que le sujet n'écrit pas et une ambiguïté formulée comme tu la dirais à la commission ;
  4. les spécifications techniques : base, route, codes de retour, règles, tests ;
  5. les outils ;
  6. le découpage vital et confort, et le plan minuté de la phase 2.
Voir la réponseMasquer la réponse de l'exercice 1

1. Reformulation. « Un comptable doit pouvoir renvoyer à son auteur une note clôturée, avec un motif obligatoire ; la note repasse en saisie, le salarié voit le motif, corrige et clôt à nouveau. »

2. Les quatre questions.

QuestionRéponse
Quoi ?une route de renvoi réservée aux comptables, le stockage du motif, sa lecture par le salarié
Où ?la table note_frais ; routes/notes.js ; controllers/notes.js (nouvelle fonction renvoyerNote, et lireNote pour le motif) ; NoteDetail.jsx dans le front, en confort
Quel impact ?la clôture existante accepte déjà une note en saisie, rien à changer ; la note renvoyée doit perdre sa date de clôture ; les 31 tests existants doivent rester verts
Quelles compétences ?maintenance évolutive (dominante), gérer les données, concevoir et développer

3. La grille des six axes.

AxeImplicite et décision
CanalPATCH /api/notes/:id/renvoi avec le motif dans le corps JSON ; un bouton dans le front en confort
Déclencheurl'action du comptable ; précondition : la note est cloturee
Acteurs et droitsseul un comptable renvoie ; le salarié lit le motif de sa note seulement, par la route de détail déjà protégée
Cas limitesnote en saisie, validée ou remboursée : 409 ; note inexistante : 404 ; motif vide, trop court ou trop long : 400 ; deux comptables qui renvoient la même note au même instant ; une note renvoyée deux fois
Donnéesmotif de 10 à 500 caractères ; date_cloture remise à NULL, sinon la note compterait dans le mois de sa première clôture ; montant_valide reste vide
Sécurité et conformitéle motif parle du travail du salarié : il reste factuel, lisible par lui et par les comptables ; requêtes préparées ; React échappe l'affichage du texte

Les deux implicites que le sujet n'écrit pas : la date de clôture à effacer, et le renvoi simultané par deux comptables.

Ambiguïté, telle que tu la dis. « Le besoin demande le motif du dernier renvoi. Je retiens une colonne motif_renvoi sur la note, qui garde le dernier motif : c'est le plus simple et cela répond à la demande. Si vous voulez l'historique de tous les renvois, il faut une table renvoi, avec la date et le comptable, et une transaction pour écrire dans les deux tables. »

4. Spécifications techniques.

  • Base : ALTER TABLE note_frais ADD COLUMN motif_renvoi VARCHAR(500) NULL; (facultative : les notes existantes n'en ont pas).
  • Route : PATCH /api/notes/:id/renvoi, middlewares requireAuth puis requireRole('comptable'). Corps : { "motif": "…" }.
  • Effet : etat = 'saisie', date_cloture = NULL, motif_renvoi = motif, en une seule requête UPDATE … WHERE id_note = ? AND etat = 'cloturee'. Si aucune ligne n'est modifiée, une lecture de l'état distingue 404 et 409.
  • Codes : 200 avec la note, 400 motif invalide, 401 sans jeton, 403 salarié, 404 note absente, 409 note non clôturée.
  • Lecture : GET /api/notes/:id renvoie aussi motif_renvoi.
  • Tests : supertest pour 401, 403 et 400 ; curl pour 404, 409 et le cas nominal ; résultats dans le rapport de test.

5. Outils. Ceux du projet : Express 5, mysql2, Vitest et supertest, curl et le client MySQL. Aucune installation.

6. Vital, confort et plan.

VitalConfort
migration et route protégéebouton « Renvoyer » avec saisie du motif
contrôles 400, 404, 409 et effet sur la noteaffichage du motif dans NoteDetail.jsx
motif lisible dans GET /api/notes/:idnotification du salarié
trois tests automatisés et rapport de test

Plan : 0 à 5, relire et sauvegarder la base ; 5 à 12, migration ; 12 à 30, route et contrôles testés avec curl ; 30 à 38, motif dans la lecture ; 38 à 48, tests ; 48 à 55, rapport ; 55 à 60, préparer la recette.

Tu avais trouvé ?

Phase 1 : l'entretien

Exercice 2 sur 4 Les questions de la commission Réussi À revoir

Après ton exposé, la commission te pose ces questions. Réponds en deux à quatre phrases à chacune.

  1. « Pourquoi remettre la date de clôture à NULL ? »
  2. « Deux comptables cliquent sur Renvoyer au même instant. Que se passe-t-il ? »
  3. « Pourquoi 409 et pas 400 pour une note validée ? »
  4. « Le salarié peut-il modifier le motif ? »
  5. « Une colonne vous suffit vraiment ? »
  6. « Comment saurez-vous que vous n'avez rien cassé ? »
Voir la réponseMasquer la réponse de l'exercice 2
  1. « La note n'est plus clôturée : garder la date serait faux. Les listes et les statistiques par date de clôture la compteraient dans le mois de sa première clôture. Quand le salarié la clôturera de nouveau, la route existante enregistrera la nouvelle date. »
  2. « Ma requête modifie la note seulement si elle est encore cloturee, et MySQL exécute ce UPDATE de façon atomique. Le premier renvoi passe ; le second ne modifie aucune ligne, ma route relit l'état, trouve saisie et répond 409. Aucun renvoi en double. »
  3. « La requête est bien formée et la note existe : ce n'est ni un 400 ni un 404. C'est l'état actuel de la ressource qui interdit l'opération, ce que signale 409 Conflict. Le message indique l'état actuel pour que le comptable comprenne. »
  4. « Non. Seule la route de renvoi écrit le motif, et elle est réservée aux comptables. Le salarié le lit par la route de détail, qui vérifie déjà qu'il est propriétaire de la note. »
  5. « Pour la demande telle qu'elle est écrite, oui : le salarié voit le dernier motif. Mais une colonne efface le motif précédent à chaque renvoi : si le service veut savoir combien de fois une note a été renvoyée et par qui, il faut une table. Je peux faire l'un ou l'autre dans l'heure. »
  6. « Je relance les 31 tests existants après chaque étape, en particulier ceux de la clôture et de la validation. J'ajoute trois tests pour la nouvelle route, et je vérifie à la main qu'une note renvoyée peut être clôturée de nouveau. »

Tu avais trouvé ?

L'ajustement de la commission

Ne lis ce paragraphe qu'après l'entretien. La commission écrit au verso de l'expression des besoins :

« La commission souhaite conserver l'historique de chaque renvoi : date, comptable, motif. Une note peut être renvoyée plus d'une fois. Le salarié voit toujours le motif du dernier renvoi. »

Ta réponse 5 a ouvert la porte : l'ajustement retient l'autre option. Ton modèle de données change, ta route aussi.

Phase 2 : la réalisation

Exercice 3 sur 4 Réalise la solution ajustée (1 heure) Réussi À revoir

Livre, en une heure :

  1. le plan corrigé par l'ajustement ;
  2. la migration de la base et son annulation ;
  3. la route et son contrôleur ;
  4. la lecture du dernier motif ;
  5. trois tests automatisés ;
  6. le rapport de test ;
  7. ton historique Git.
Voir la réponseMasquer la réponse de l'exercice 3

1. Le plan corrigé. La colonne devient une table renvoi. Le renvoi écrit maintenant dans deux tables : il faut une transaction. L'UPDATE conditionnel ne suffit plus pour distinguer les cas avant d'insérer, on verrouille la note avec SELECT … FOR UPDATE.

2. La migration.

sql
-- migrations/2027-04-renvoi.sql
CREATE TABLE renvoi (
  id_renvoi    INT AUTO_INCREMENT PRIMARY KEY,
  id_note      INT          NOT NULL,
  id_comptable INT          NOT NULL,
  date_renvoi  DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  motif        VARCHAR(500) NOT NULL,
  CONSTRAINT ck_renvoi_motif CHECK (CHAR_LENGTH(motif) >= 10),
  CONSTRAINT fk_renvoi_note FOREIGN KEY (id_note) REFERENCES note_frais (id_note),
  CONSTRAINT fk_renvoi_comptable FOREIGN KEY (id_comptable) REFERENCES utilisateur (id_utilisateur)
);
-- Annulation : DROP TABLE renvoi;

3. La route et le contrôleur.

js
// routes/notes.js
router.patch('/:id/renvoi', requireAuth, requireRole('comptable'), renvoyerNote);
js
// controllers/notes.js
const MOTIF_MIN = 10;
const MOTIF_MAX = 500;

export async function renvoyerNote(req, res) {
  const idNote = Number(req.params.id);
  const motif = typeof req.body?.motif === 'string' ? req.body.motif.trim() : '';
  if (!Number.isInteger(idNote)) return res.status(400).json({ erreur: 'Identifiant de note invalide' });
  if (motif.length < MOTIF_MIN || motif.length > MOTIF_MAX) {
    return res.status(400).json({ erreur: `Le motif doit faire entre ${MOTIF_MIN} et ${MOTIF_MAX} caractères` });
  }

  const conn = await pool.getConnection();
  try {
    await conn.beginTransaction();
    // FOR UPDATE verrouille la note : un second renvoi simultané attend la fin de celui-ci.
    const [notes] = await conn.execute('SELECT etat FROM note_frais WHERE id_note = ? FOR UPDATE', [idNote]);
    if (notes.length === 0) {
      await conn.rollback();
      return res.status(404).json({ erreur: 'Note introuvable' });
    }
    if (notes[0].etat !== 'cloturee') {
      await conn.rollback();
      return res.status(409).json({ erreur: `Seule une note clôturée peut être renvoyée (état actuel : ${notes[0].etat})` });
    }
    await conn.execute("UPDATE note_frais SET etat = 'saisie', date_cloture = NULL WHERE id_note = ?", [idNote]);
    await conn.execute('INSERT INTO renvoi (id_note, id_comptable, motif) VALUES (?, ?, ?)', [idNote, req.user.id, motif]);
    await conn.commit();
    res.json({ id_note: idNote, etat: 'saisie', motif });
  } catch (err) {
    await conn.rollback();
    throw err; // Express 5 transmet l'erreur au middleware d'erreur : réponse 500
  } finally {
    conn.release();
  }
}

4. La lecture du dernier motif, dans la requête de lireNote (le contrôle de propriété ne change pas) :

sql
SELECT n.id_note, n.id_utilisateur, n.mois, n.etat, n.montant_valide,
       (SELECT r.motif FROM renvoi r
         WHERE r.id_note = n.id_note
         ORDER BY r.date_renvoi DESC, r.id_renvoi DESC
         LIMIT 1) AS dernier_motif
  FROM note_frais n
 WHERE n.id_note = ?

Le second critère de tri départage deux renvois dans la même seconde.

5. Les tests.

js
// tests/renvoi.test.js
import jwt from 'jsonwebtoken';
import request from 'supertest';
import { expect, test } from 'vitest';
import { app } from '../app.js';

const jeton = (role) => jwt.sign({ id: 3, role }, process.env.JWT_SECRET, { expiresIn: '5m' });
const renvoyer = (role, corps) =>
  request(app).patch('/api/notes/41/renvoi').set('Authorization', `Bearer ${jeton(role)}`).send(corps);

test('sans jeton : 401', async () => {
  const res = await request(app).patch('/api/notes/41/renvoi').send({ motif: 'Ticket illisible.' });
  expect(res.status).toBe(401);
});

test('un salarié ne peut pas renvoyer : 403', async () => {
  expect((await renvoyer('salarie', { motif: 'Ticket illisible.' })).status).toBe(403);
});

test('un motif trop court est refusé : 400', async () => {
  expect((await renvoyer('comptable', { motif: 'flou' })).status).toBe(400);
});

6. Le rapport de test.

N°CasDonnéesAttenduObtenuConforme
1sans jetonaucun en-tête Authorization401401oui
2rôle salariéjeton de salarie1403403oui
3motif trop court« flou »400400oui
4note inexistantenote 9999404404oui
5note en saisienote 37409, état cité409, « état actuel : saisie »oui
6cas nominalnote 41 clôturée, motif de 54 caractères200 ; note en saisie, date de clôture vide ; une ligne dans renvoiconformeoui
7second renvoinote 41 clôturée de nouveau, autre motifdeux lignes dans renvoi, le détail montre le second motifconformeoui
8non-régressionnpm testtous les tests verts34 tests vertsoui

Tests 1 à 3 automatisés ; tests 4 à 7 faits avec curl et le client MySQL.

7. L'historique.

text
$ git log --oneline
a41c2e9 renvoi : tests et rapport de test
7f03b1d renvoi : dernier motif dans le détail d'une note
2c9e8a4 renvoi : route PATCH /api/notes/:id/renvoi
9b1d6f0 renvoi : table renvoi (migration)

Le bouton du front n'est pas fait : c'était du confort, et le vital, les tests et le rapport ont pris l'heure.

Tu avais trouvé ?

La recette et l'entretien d'explicitation

Exercice 4 sur 4 Ta recette et tes réponses Réussi À revoir

  1. Écris le déroulé de ta présentation de trois minutes : ce que tu dis, ce que tu montres.
  2. Réponds aux questions de la commission :
    • « Pourquoi FOR UPDATE alors que vous êtes déjà dans une transaction ? »
    • « Pourquoi une contrainte CHECK sur le motif, alors que la route le vérifie ? »
    • « Pourquoi une sous-requête pour le dernier motif ? »
    • « Le salarié ne voit pas encore le motif à l'écran. Comment le feriez-vous ? »
Voir la réponseMasquer la réponse de l'exercice 4

1. Le déroulé.

Ce que tu disCe que tu montres
« La demande : renvoyer une note clôturée avec un motif, et, après l'ajustement, garder l'historique de chaque renvoi. »le recto et le verso de l'expression des besoins
« J'ai ajouté une table renvoi et une route réservée aux comptables, qui écrit dans les deux tables en une transaction. »la migration, puis renvoyerNote
« Le cas nominal : la note 41 est clôturée, je la renvoie. »curl, réponse 200, puis SELECT sur note_frais et renvoi
« Le refus : la même note, maintenant en saisie. »curl, réponse 409 avec l'état cité
« Le salarié ne peut pas renvoyer, et les tests le prouvent. »npm test, 34 tests verts ; le rapport de test
« J'ai avancé par étapes testées. »git log --oneline
« Il reste le bouton et l'affichage dans le front. Voici comment je les ferais. »NoteDetail.jsx

2. Les réponses.

  • FOR UPDATE. « La transaction garantit que les deux écritures sont faites ensemble ou pas du tout, mais elle n'empêche pas deux comptables de lire en même temps l'état cloturee et de renvoyer tous les deux. FOR UPDATE verrouille la ligne de la note : le second attend la fin du premier, puis lit saisie et reçoit 409. »
  • La contrainte CHECK. « La route n'est pas le seul chemin vers la base : un script de reprise ou une autre application peuvent écrire dans renvoi. La contrainte est le dernier rempart. MySQL applique les contraintes CHECK depuis la version 8.0.16. »
  • La sous-requête. « Elle renvoie une seule valeur par note, le motif le plus récent, sans changer le reste de la requête de détail. Une jointure sur renvoi donnerait une ligne par renvoi. Le second critère de tri départage deux renvois dans la même seconde. »
  • L'affichage. « GET /api/notes/:id renvoie déjà dernier_motif. Dans NoteDetail.jsx, j'afficherais un encadré quand la note est en saisie et que dernier_motif n'est pas vide. React échappe le texte, donc un motif qui contiendrait du HTML s'afficherait tel quel, sans être exécuté. »

Tu avais trouvé ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Épreuve blanche E6 n° 2 : contrôler les licencesRevoir le sommaire du cours