;; lib/dream/tests/form.sx — urlencoded parsing, Ok/Err, CSRF accept/reject, multipart. (define dream-fo-pass 0) (define dream-fo-fail 0) (define dream-fo-fails (list)) (define dream-fo-test (fn (name actual expected) (if (= actual expected) (set! dream-fo-pass (+ dream-fo-pass 1)) (begin (set! dream-fo-fail (+ dream-fo-fail 1)) (append! dream-fo-fails {:name name :actual actual :expected expected}))))) ;; ── Result ───────────────────────────────────────────────────────── (dream-fo-test "ok? on ok" (dream-ok? (dream-ok 5)) true) (dream-fo-test "err? on ok" (dream-err? (dream-ok 5)) false) (dream-fo-test "ok value" (dream-ok-value (dream-ok {:a 1})) {:a 1}) (dream-fo-test "err reason" (dream-err-reason (dream-err :bad)) "bad") ;; ── urlencoded parsing ───────────────────────────────────────────── (define dream-fo-req (fn (body) (dream-request "POST" "/f" {:Content-Type "application/x-www-form-urlencoded"} body))) (dream-fo-test "parse two fields" (dream-form-fields (dream-fo-req "a=1&b=2")) {:a "1" :b "2"}) (dream-fo-test "url-decoded value" (dream-form-field (dream-fo-req "name=Ada+Lovelace") "name") "Ada Lovelace") (dream-fo-test "percent decode" (dream-form-field (dream-fo-req "x=a%20b%21") "x") "a b!") (dream-fo-test "empty body" (dream-form-fields (dream-fo-req "")) {}) (dream-fo-test "valueless key" (dream-form-field (dream-fo-req "flag") "flag") "") (dream-fo-test "decoded key" (dream-form-field (dream-fo-req "first%20name=x") "first name") "x") ;; ── CSRF sign + verify ───────────────────────────────────────────── (dream-fo-test "sign deterministic" (= (dream-csrf-sign-default "secret" "s1") (dream-csrf-sign-default "secret" "s1")) true) (dream-fo-test "sign secret-sensitive" (= (dream-csrf-sign-default "secret" "s1") (dream-csrf-sign-default "other" "s1")) false) (dream-fo-test "sign session-sensitive" (= (dream-csrf-sign-default "secret" "s1") (dream-csrf-sign-default "secret" "s2")) false) (dream-fo-test "token valid for own session" (dr/csrf-valid? dream-csrf-sign-default "k" "s1" (dr/csrf-make-token dream-csrf-sign-default "k" "s1")) true) (dream-fo-test "token invalid for other session" (dr/csrf-valid? dream-csrf-sign-default "k" "s2" (dr/csrf-make-token dream-csrf-sign-default "k" "s1")) false) (dream-fo-test "tampered token invalid" (dr/csrf-valid? dream-csrf-sign-default "k" "s1" "s1.deadbeef") false) (dream-fo-test "empty token invalid" (dr/csrf-valid? dream-csrf-sign-default "k" "s1" "") false) (dream-fo-test "nil token invalid" (dr/csrf-valid? dream-csrf-sign-default "k" "s1" nil) false) ;; ── full stack: session -> csrf -> handler ───────────────────────── (define dream-fo-backend (dream-memory-sessions)) (define dream-fo-sid (dream-fo-backend {:op "session/create"})) ;; s1 (define dream-fo-stack (fn (handler) ((dream-sessions dream-fo-backend) ((dream-csrf "topsecret") handler)))) (define dream-fo-tag-out (dream-resp-body ((dream-fo-stack (fn (req) (dream-text (dream-csrf-tag req)))) (dream-request "GET" "/form" {:Cookie "dream.session=s1"} "")))) (dream-fo-test "csrf-tag is hidden input" (contains? dream-fo-tag-out "type=\"hidden\"") true) (dream-fo-test "csrf-tag names field" (contains? dream-fo-tag-out "name=\"dream.csrf\"") true) (define dream-fo-good-token (dr/csrf-make-token dream-csrf-sign-default "topsecret" "s1")) (define dream-fo-submit (fn (token) ((dream-fo-stack (fn (req) (let ((r (dream-form req))) (if (dream-ok? r) (dream-text (str "ok:" (get (dream-ok-value r) "msg"))) (dream-text (str "err:" (dream-err-reason r))))))) (dream-request "POST" "/form" {:Cookie "dream.session=s1"} (str "msg=hello&dream.csrf=" token))))) (dream-fo-test "valid csrf -> Ok fields" (dream-resp-body (dream-fo-submit dream-fo-good-token)) "ok:hello") (dream-fo-test "bad csrf -> Err" (dream-resp-body (dream-fo-submit "s1.wrong")) "err:csrf-token-invalid") (dream-fo-test "missing csrf -> Err" (dream-resp-body (dream-fo-submit "")) "err:csrf-token-invalid") ;; ── csrf-protect middleware auto-rejects ─────────────────────────── (define dream-fo-protected (fn (handler) ((dream-sessions dream-fo-backend) ((dream-csrf-protect "topsecret") handler)))) (define dream-fo-ph (dream-fo-protected (fn (req) (dream-text "reached")))) (dream-fo-test "GET passes without token" (dream-resp-body (dream-fo-ph (dream-request "GET" "/x" {:Cookie "dream.session=s1"} ""))) "reached") (dream-fo-test "POST without token 403" (dream-status (dream-fo-ph (dream-request "POST" "/x" {:Cookie "dream.session=s1"} ""))) 403) (dream-fo-test "POST with valid token reaches" (dream-resp-body (dream-fo-ph (dream-request "POST" "/x" {:Cookie "dream.session=s1"} (str "dream.csrf=" dream-fo-good-token)))) "reached") ;; ── multipart/form-data ──────────────────────────────────────────── (define dream-fo-mp-body (str "--B1\r\n" "Content-Disposition: form-data; name=\"title\"\r\n\r\n" "Hello\r\n" "--B1\r\n" "Content-Disposition: form-data; name=\"file\"; filename=\"a.txt\"\r\nContent-Type: text/plain\r\n\r\n" "line1\r\nline2\r\n" "--B1--\r\n")) (define dream-fo-mp-req (dream-request "POST" "/upload" {:Content-Type "multipart/form-data; boundary=B1"} dream-fo-mp-body)) (define dream-fo-mp (dream-multipart dream-fo-mp-req)) (dream-fo-test "multipart is Ok" (dream-ok? dream-fo-mp) true) (define dream-fo-parts (dream-ok-value dream-fo-mp)) (dream-fo-test "two parts" (len dream-fo-parts) 2) (dream-fo-test "field value" (dream-multipart-field dream-fo-parts "title") "Hello") (dream-fo-test "file part filename" (get (dream-multipart-file dream-fo-parts "file") :filename) "a.txt") (dream-fo-test "file content-type" (get (dream-multipart-file dream-fo-parts "file") :content-type) "text/plain") (dream-fo-test "file content keeps inner CRLF" (get (dream-multipart-file dream-fo-parts "file") :content) "line1\r\nline2") (dream-fo-test "field is not a file" (get (dream-multipart-file dream-fo-parts "title") :filename) nil) (dream-fo-test "non-multipart is Err" (dream-err? (dream-multipart (dream-request "POST" "/x" {:Content-Type "text/plain"} "hi"))) true) (dream-fo-test "quoted boundary parsed" (dream-ok? (dream-multipart (dream-request "POST" "/u" {:Content-Type "multipart/form-data; boundary=\"B1\""} dream-fo-mp-body))) true) (define dream-fo-tests-run! (fn () {:total (+ dream-fo-pass dream-fo-fail) :passed dream-fo-pass :failed dream-fo-fail :fails dream-fo-fails}))