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.
| Temps | Durée | Ce que tu produis |
|---|---|---|
| Préparation de la phase 1 | 30 min | ta feuille : reformulation, implicite, spécifications, outils, plan |
| Entretien de la phase 1 | 20 min | ton exposé, puis tes réponses aux questions |
| Préparation de la phase 2 | 1 h | le code, les tests, le rapport de test, l'historique Git |
| Recette et entretien | 20 min | ta 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 :
| Route | Rôle | Effet |
|---|---|---|
GET /api/notes | salarié ou comptable | ses notes pour un salarié, toutes pour un comptable |
GET /api/notes/:id | propriétaire ou comptable | le détail d'une note |
PATCH /api/notes/:id/cloture | salarié propriétaire | saisie vers cloturee, date_cloture renseignée |
PATCH /api/notes/:id/validation | comptable | cloturee 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.
- 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.
- Une note renvoyée repasse à l'état « saisie » : le salarié peut la corriger, puis la clôturer à nouveau.
- Le salarié voit le motif du dernier renvoi de sa note.
- 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 :
- la reformulation en une phrase ;
- les quatre questions : quoi, où, quel impact, quelles compétences ;
- 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 ;
- les spécifications techniques : base, route, codes de retour, règles, tests ;
- les outils ;
- 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.
| Question | Ré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.
| Axe | Implicite et décision |
|---|---|
| Canal | PATCH /api/notes/:id/renvoi avec le motif dans le corps JSON ; un bouton dans le front en confort |
| Déclencheur | l'action du comptable ; précondition : la note est cloturee |
| Acteurs et droits | seul un comptable renvoie ; le salarié lit le motif de sa note seulement, par la route de détail déjà protégée |
| Cas limites | note 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ées | motif 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, middlewaresrequireAuthpuisrequireRole('comptable'). Corps :{ "motif": "…" }. - Effet :
etat = 'saisie',date_cloture = NULL,motif_renvoi = motif, en une seule requêteUPDATE … 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/:idrenvoie aussimotif_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.
| Vital | Confort |
|---|---|
| migration et route protégée | bouton « Renvoyer » avec saisie du motif |
| contrôles 400, 404, 409 et effet sur la note | affichage du motif dans NoteDetail.jsx |
motif lisible dans GET /api/notes/:id | notification 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.
- « Pourquoi remettre la date de clôture à
NULL? » - « Deux comptables cliquent sur Renvoyer au même instant. Que se passe-t-il ? »
- « Pourquoi 409 et pas 400 pour une note validée ? »
- « Le salarié peut-il modifier le motif ? »
- « Une colonne vous suffit vraiment ? »
- « Comment saurez-vous que vous n'avez rien cassé ? »
Voir la réponseMasquer la réponse de l'exercice 2
- « 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. »
- « Ma requête modifie la note seulement si elle est encore
cloturee, et MySQL exécute ceUPDATEde façon atomique. Le premier renvoi passe ; le second ne modifie aucune ligne, ma route relit l'état, trouvesaisieet répond 409. Aucun renvoi en double. » - « 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. »
- « 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. »
- « 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. »
- « 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 :
- le plan corrigé par l'ajustement ;
- la migration de la base et son annulation ;
- la route et son contrôleur ;
- la lecture du dernier motif ;
- trois tests automatisés ;
- le rapport de test ;
- 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.
-- 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.
// routes/notes.js
router.patch('/:id/renvoi', requireAuth, requireRole('comptable'), renvoyerNote);
// 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) :
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.
// 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° | Cas | Données | Attendu | Obtenu | Conforme |
|---|---|---|---|---|---|
| 1 | sans jeton | aucun en-tête Authorization | 401 | 401 | oui |
| 2 | rôle salarié | jeton de salarie1 | 403 | 403 | oui |
| 3 | motif trop court | « flou » | 400 | 400 | oui |
| 4 | note inexistante | note 9999 | 404 | 404 | oui |
| 5 | note en saisie | note 37 | 409, état cité | 409, « état actuel : saisie » | oui |
| 6 | cas nominal | note 41 clôturée, motif de 54 caractères | 200 ; note en saisie, date de clôture vide ; une ligne dans renvoi | conforme | oui |
| 7 | second renvoi | note 41 clôturée de nouveau, autre motif | deux lignes dans renvoi, le détail montre le second motif | conforme | oui |
| 8 | non-régression | npm test | tous les tests verts | 34 tests verts | oui |
Tests 1 à 3 automatisés ; tests 4 à 7 faits avec curl et le client MySQL.
7. L'historique.
$ 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
- Écris le déroulé de ta présentation de trois minutes : ce que tu dis, ce que tu montres.
- Réponds aux questions de la commission :
- « Pourquoi
FOR UPDATEalors que vous êtes déjà dans une transaction ? » - « Pourquoi une contrainte
CHECKsur 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 ? »
- « Pourquoi
Voir la réponseMasquer la réponse de l'exercice 4
1. Le déroulé.
| Ce que tu dis | Ce 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'étatclotureeet de renvoyer tous les deux.FOR UPDATEverrouille la ligne de la note : le second attend la fin du premier, puis litsaisieet 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 dansrenvoi. La contrainte est le dernier rempart. MySQL applique les contraintesCHECKdepuis 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
renvoidonnerait une ligne par renvoi. Le second critère de tri départage deux renvois dans la même seconde. » - L'affichage. «
GET /api/notes/:idrenvoie déjàdernier_motif. DansNoteDetail.jsx, j'afficherais un encadré quand la note est en saisie et quedernier_motifn'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é ?