Files
NimPulse/tests/test_webserver.nim
T
bruno b47bf61386 feat(web): éditeur de périodes modifiables dans les réglages
La page Réglages permet maintenant de créer, modifier et supprimer ses propres périodes (profils) : nom, travail, pause courte, pause longue et nombre de cycles, avec bouton radio ★ pour choisir la période active. Enregistrement via profiles_json + active_profile (index). Repli automatique sur la première période si l'active est supprimée ; noms vides/doublons ignorés. La page Aide affiche le tableau des périodes réelles de la configuration. Tests : parsing, édition, repli du profil actif et intégration HTTP.
2026-08-14 23:37:23 -04:00

173 lines
6.8 KiB
Nim

## test_webserver.nim
## ==================
## Tests du module webserver : parsing HTTP, décodage du formulaire,
## application des réglages, rendu des pages et intégration réelle
## (serveur local 127.0.0.1 interrogé en HTTP).
import std/[unittest, httpclient, os, uri, tables, strutils]
import ../src/webserver
import ../src/config
proc tmpDir(): string =
getAppFilename().parentDir / ".webdata_test"
suite "http helpers":
test "parseRequest parses GET with query":
let req = parseRequest("GET /help?lang=fr HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n")
check req.verb == "GET"
check req.path == "/help"
check req.body == ""
test "parseRequest parses POST with body":
let req = parseRequest(
"POST /settings HTTP/1.1\r\nContent-Length: 10\r\n\r\nworkMin=30")
check req.verb == "POST"
check req.path == "/settings"
check req.body == "workMin=30"
test "parseRequest decodes percent-encoding":
let req = parseRequest("GET /settings%3Fx HTTP/1.1\r\n\r\n")
check req.path == "/settings?x"
test "htmlEscape escapes special characters":
check htmlEscape("<a href=\"x\">&'") == "&lt;a href=&quot;x&quot;&gt;&amp;&#39;"
test "parseForm decodes urlencoded pairs":
let f = parseForm("workMin=30&lang=fr&soundEnabled=on&note=Hello%20world")
check f["workMin"] == "30"
check f["lang"] == "fr"
check f["soundEnabled"] == "on"
check f["note"] == "Hello world"
suite "applySettings":
test "applies a full form":
let base = defaultConfig()
let form = parseForm("workMin=40&shortBreakMin=8&longBreakMin=20&" &
"cyclesBeforeLongBreak=5&dailyGoal=6&lang=en&" &
"active_profile=1&" &
"profiles_json=" & encodeUrl(
"""[{"name":"Focus","workMin":30,"shortBreakMin":6,"longBreakMin":18,"cycles":4},{"name":"Rapide","workMin":10,"shortBreakMin":2,"longBreakMin":5,"cycles":3}]""") &
"&soundEnabled=on&doNotDisturb=on&" &
"reminders_json=" & encodeUrl("""[{"text":"Boire de l'eau","enabled":true}]"""))
let cfg = applySettings(base, form)
check cfg.workMin == 40
check cfg.shortBreakMin == 8
check cfg.longBreakMin == 20
check cfg.cyclesBeforeLongBreak == 5
check cfg.dailyGoal == 6
check cfg.lang == "en"
check cfg.profiles.len == 2
check cfg.profiles[0].name == "Focus"
check cfg.profiles[1].workMin == 10
check cfg.activeProfile == "Rapide"
check cfg.soundEnabled
check cfg.doNotDisturb
check not cfg.autoStart
check cfg.healthReminders.len == 1
check cfg.healthReminders[0].text == "Boire de l'eau"
test "profiles are edited and active profile falls back safely":
let base = defaultConfig()
# Deux périodes, active = index 1.
let f1 = parseForm("profiles_json=" & encodeUrl(
"""[{"name":"A","workMin":25,"shortBreakMin":5,"longBreakMin":15,"cycles":4},{"name":"B","workMin":50,"shortBreakMin":10,"longBreakMin":20,"cycles":5}]""") &
"&active_profile=1")
let c1 = applySettings(base, f1)
check c1.profiles.len == 2
check c1.activeProfile == "B"
# Index hors limites (période active supprimée) : repli sur la première.
let f2 = parseForm("profiles_json=" & encodeUrl(
"""[{"name":"C","workMin":20,"shortBreakMin":4,"longBreakMin":10,"cycles":3}]""") &
"&active_profile=5")
let c2 = applySettings(base, f2)
check c2.profiles.len == 1
check c2.activeProfile == "C"
# Noms vides et doublons ignorés.
let f3 = parseForm("profiles_json=" & encodeUrl(
"""[{"name":"","workMin":1,"shortBreakMin":1,"longBreakMin":1,"cycles":1},{"name":"D","workMin":33,"shortBreakMin":3,"longBreakMin":9,"cycles":2},{"name":"D","workMin":99,"shortBreakMin":9,"longBreakMin":27,"cycles":9}]"""))
let c3 = applySettings(base, f3)
check c3.profiles.len == 1
check c3.profiles[0].name == "D"
check c3.profiles[0].workMin == 33
test "invalid values fall back to current ones":
let base = defaultConfig()
let form = parseForm("workMin=abc&lang=zz&active_profile=99")
let cfg = applySettings(base, form)
check cfg.workMin == base.workMin
check cfg.lang == "auto"
check cfg.activeProfile == base.activeProfile
suite "rendering":
test "pages contain expected content":
var cfg = defaultConfig()
cfg.lang = "fr" # langue fixe : le rendu ne dépend pas du système
check "NimPulse" in renderHelp(cfg)
check "Pomodoro" in renderHelp(cfg)
check "Statistiques" in renderStats(cfg)
check "Réglages" in renderSettings(cfg)
check "name='workMin'" in renderSettings(cfg)
check "reminders_json" in renderSettings(cfg)
check "profiles_json" in renderSettings(cfg)
check "active_profile" in renderSettings(cfg)
suite "integration (local HTTP server)":
test "GET routes work and settings POST persists":
let dir = tmpDir()
removeDir(dir)
createDir(dir)
# Configuration initiale en français : le rendu est déterministe.
var initCfg = defaultConfig()
initCfg.lang = "fr"
saveConfigTo(initCfg, dir / "config.json")
setDataDirOverride(dir)
defer: setDataDirOverride("")
let port = startWebServer()
check port > 0
defer: stopWebServer()
let base = "http://127.0.0.1:" & $port
let client = newHttpClient()
let help = client.getContent(base & "/help")
check "NimPulse" in help
let stats = client.getContent(base & "/stats")
check "Statistiques" in stats
let settings = client.getContent(base & "/settings")
check "name='workMin'" in settings
# Sauvegarde via POST (comme le ferait le navigateur).
let body = "workMin=33&shortBreakMin=6&longBreakMin=18&cyclesBeforeLongBreak=4&" &
"dailyGoal=5&lang=fr&active_profile=1&" &
"profiles_json=" & encodeUrl(
"""[{"name":"Zen","workMin":35,"shortBreakMin":7,"longBreakMin":20,"cycles":4},{"name":"Bolt","workMin":5,"shortBreakMin":1,"longBreakMin":3,"cycles":2}]""") &
"&reminders_json=" & encodeUrl(
"""[{"text":"Étire-toi","enabled":true},{"text":"Bois de l'eau","enabled":false}]""")
let resp = client.postContent(base & "/settings", body)
check "Réglages enregistrés" in resp
# Le fichier config.json a été écrit dans le répertoire de test.
let saved = loadConfigFrom(dir / "config.json")
check saved.workMin == 33
check saved.dailyGoal == 5
check saved.lang == "fr"
check saved.profiles.len == 2
check saved.profiles[0].name == "Zen"
check saved.profiles[1].workMin == 5
check saved.activeProfile == "Bolt"
check saved.healthReminders.len == 2
check saved.healthReminders[1].enabled == false
# Le drapeau de rechargement est posé puis consommé.
check consumeSettingsChanged()
check not consumeSettingsChanged()
# La page réglages reflète les nouvelles valeurs.
let settings2 = client.getContent(base & "/settings")
check "value='33'" in settings2
removeDir(dir)