08.06 — Gaps data (G-DATA-1, G-DATA-2)#
Deux gaps majeurs sur l’intégrité des données.
G-DATA-1 — Pas de validation serveur de visit_date#
Source#
// appointment.php
array_diff_key($_POST, array(
"facility" => "",
"hospital_readmission" => "",
"programs" => "",
"visit_date" => "",
"comment" => ""
))Cette ligne fait juste une vérification de présence des clés.
Elle ne valide jamais la valeur de visit_date.
Symptôme#
- Empty visit_date : si on supprime l’attribut HTML5
requiredvia JS et qu’on submit avec une date vide → prise de RDV acceptée. - Past visit_date : si on envoie une réservation antidatée → prise de RDV acceptée.
- La réservation est bien ajoutée à l’historique telle quelle.
Tests associés#
| Test | Manière |
|---|---|
Appointment - Server accepts empty date when client bypass applied | Retire l’attribut required, envoie avec date de réservation vide → s’attend à être rejeté par le système (FAIL) |
Appointment - Past date booking accepted | Envoie une demande de RDV pour hier → s’attend à être rejeté par le système (FAIL) |
Les deux tests sont intentionnellement en échec.
Ils documentent le gap.
« Past date booking »#
# src/tests/scenarios/appointment/past_date_booking.py
"""Past-date booking accepted — no temporal validation (§9.7 gap).
Flow:
open appointment form → fill (facility, program, comment, date = yesterday) → submit
→ verify NOT confirmed
(intentional FAIL — CURA accepts past dates, §9.7 gap)
A healthcare scheduling system must not accept bookings for dates that have
already passed. CURA performs no temporal validation: yesterday's date is
accepted and a confirmation is returned. The test asserts rejection and fails
when the confirmation appears.
Pre-fragments: login_as_demo_user
Post-fragments: (none)
"""
def _scenario_past_date_booking(driver, logger) -> TestChain:
on_appointment = AppointmentPage(driver=driver)
on_confirmation = ConfirmationPage(driver=driver)
log_error = create_just_log_error(logger=logger)
log_success = create_just_log_success(logger=logger)
log_and_screenshot = create_log_success_and_take_screenshot(driver=driver, logger=logger)
return [
drive_page(
act(on_appointment, open_appointment_page)
.failure(log_error("Failed to open appointment form"))
.success(log_success("Opened appointment form")),
act(on_appointment, fill_appointment(
facility="Hongkong CURA Healthcare Center",
programs="Medicare",
visit_date=in_days(-1), # ← hier
comment="Past-date booking attempt",
))
.failure(log_error("Failed to fill form"))
.success(log_and_screenshot("Filled form with past date")),
act(on_appointment, submit_appointment)
.failure(log_error("Failed to submit"))
.success(log_success("Submitted")),
),
drive_page(
act(on_confirmation, verify_booking_not_confirmed)
.failure(log_error("CURA accepted past date — §9.7 gap"))
.success(log_and_screenshot("Rejection confirmed")),
),
]verify_booking_not_confirmedretourne success si on est resté sur/appointment.php(= rejet).- Si on a été redirigé vers
/confirmation.php(= acceptation),verify_booking_not_confirmedlève →.failure()log explicite « CURA accepted past date — §9.7 gap ».
verify_booking_not_confirmed#
# pages/confirmation.py
_NOT_CONFIRMED_TIMEOUT = 10 # ← fixe, indépendant de --wait-timeout
class ConfirmationPage(...):
def verify_booking_not_confirmed(self) -> ConfirmationPage:
try:
WebDriverWait(self._driver, _NOT_CONFIRMED_TIMEOUT).until(
ec.url_contains("appointment.php")
)
return self # ✅ on est resté sur appointment → rejet
except TimeoutException as exc:
raise BookingNotRejectedError(
f"CURA accepted the booking (url: {self._driver.current_url}); §9.7 gap"
) from excurl_contains("appointment.php"): test sur URL, pas sur le DOM (choix d’implémentation correspondant au comportement de CURA)._NOT_CONFIRMED_TIMEOUT = 10: fixe à 10s, indépendant de--wait-timeout.BookingNotRejectedError: exception spécifique. Pas unAssertionError. Pas unTimeoutException. Ce N’EST PAS un flake.
G-DATA-2 — Pas de contrainte d’unicité ni de vérification de conflit#
Source#
// appointment.php
array_push($_SESSION['history'], $_POST);array_push inconditionnel.
Aucune vérification :
- Pas de check : « cet utilisateur a-t-il déjà un RDV pour cette date ? ».
- Pas de check : « ces deux RDV sont-ils géographiquement compatibles ? » (Hongkong + Tokyo le même jour, au même nom).
Symptômes#
- Duplicate : deux RDV identiques → les deux sont pris et apparaissent dans l’historique.
- Overlap : deux RDV le même jour mais à des lieux très éloignés (Hongkong + Tokyo) → les deux sont pris.
Tests associés#
| Test | Manière | FAIL ? |
|---|---|---|
Appointments - Duplicate booking | Réserve A, puis réserve A encore | FAIL (CURA accepte) |
Appointments - Overlapping appointments | Réserve Hongkong/2025-05-18, réserve Tokyo/2025-05-18 | FAIL (CURA accepte) |
book_then_verify_not_confirmed
Pour ces tests, on re-utilise le pattern G-DATA-1 :
- Réserve un premier RDV (accepté).
- Réserve un second insensé (devrait être rejeté).
verify_booking_not_confirmedsur le second.
Si le second RDV est accepté aussi → fail (gap confirmé).
_NOT_CONFIRMED_TIMEOUT
Tous ces gap tests utilisent cette constante :
_NOT_CONFIRMED_TIMEOUT = 10C’est un constante qui contourne sciemment les paramètres CLI. Si quelqu’un augmente --wait-timeout à 30 pour stabiliser un autre test, les gap tests restent à 10.
Pas de risque de gonfler silencieusement la fenêtre de gap.
CURA_TEST_STRATEGY.md :
The assertion window is fixed at
_NOT_CONFIRMED_TIMEOUT(10 s, defined inconfirmation.py) and is intentionally independent of--wait-timeout: bumping the global wait flag for driver stability must not silently inflate every gap test’s assertion budget.
saturation_booking#
Example:
saturation_booking.py— 5 consecutive bookings on different dates in one session, all accepted; documents the absence of a rate limit.
C’est un test exploratoire (pas un gap test). Il passe parce qu’il documente le comportement actuel (CURA accepte 5 prises de RDV rapides). Si CURA un jour ajoutait un rate-limit qui rejette le 5e, ce test casserait → on réviserait le test pour refléter la nouvelle réalité.
Pédagogie#
| Concept | Test |
|---|---|
| Comment exprimer un gap test | past_date_booking.py |
| Comment gérer un timeout indépendant | _NOT_CONFIRMED_TIMEOUT |
| Comment distinguer une exception « SUT défaillant » d’un « comportement attendu » | BookingNotRejectedError vs TimeoutException |
| Comment documenter un comportement observé sans gap assertion | saturation_booking.py |