Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 53s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
227 lines
7.2 KiB
Plaintext
227 lines
7.2 KiB
Plaintext
;; 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}))
|