## 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">&'" test "parseForm decodes urlencoded pairs": let f = parseForm("workMin=30&lang=fr&soundEnabled=on¬e=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)