mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 05:50:35 +00:00
* 🚑 Reject forged upload paths outside multipart Transit bodies can smuggle upload maps whose path points at any server file; the schema only checks the shape and handlers open the path directly. app.rpc now injects a server-side multipart flag that bodies cannot forge, and methods declare their upload keys with the new multipart-only-params metadata, enforced centrally after schema validation with a params-validation error. Covers all nine upload methods, including chunked and management flows. AI-assisted-by: muse-spark-1.3-contributor * 🐛 Sanitize raw explain in generic validation error bodies The generic :validation branch returned ex-data untouched, so errors carrying the raw malli explain (live schema objects, e.g. storage invalid-storage-metadata) broke JSON encoding mid-stream and clients got truncated bodies. Apply the same clean treatment the known codes already use: drop the raw explain and attach its humanized text. AI-assisted-by: muse-spark-1.3-contributor * 🐛 Add e2e path traversal tests for binfile and media upload Add two e2e suites that hand-write transit bodies with a forged ["~#path", ...] upload path (no transit library needed) and expect 400 validation/params-validation. They cover import-binfile :file, upload-file-media-object :content and upload-chunk :content, plus a plain-JSON string-path control case. Before the multipart-path fix they failed (500 io-exception, invalid-image, 200 stored chunk); after it they pass. AI-assisted-by: muse-spark-1.3-contributor * 🐛 Use multipart helper in binfile SVG test setup The SVG import setup uploaded through plain command!, so the new multipart-only guard rejected it with params-validation. Switch the setup to multipart-command! like the other upload tests. AI-assisted-by: muse-spark-1.3-contributor * 🐛 Use multipart helper in remaining upload tests The multipart-only upload guard rejects plain RPC calls carrying upload maps. Five tests with real files still used command! and failed with params-validation. Switch them to multipart-command! while keeping the forged-content tests on plain command!. AI-assisted-by: muse-spark-1.3-contributor
154 lines
6.4 KiB
Clojure
154 lines
6.4 KiB
Clojure
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
;;
|
|
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
|
|
|
(ns backend-tests.helpers-test
|
|
"Focused unit tests for the shared test request constructor.
|
|
|
|
`make-dummy-request` is called with three styles across the suite
|
|
(no arguments, a single options map, and keyword arguments). These
|
|
tests pin that contract so a future redefinition or reordering of
|
|
the helper fails loudly here instead of as a flood of unrelated
|
|
test errors."
|
|
(:require
|
|
[backend-tests.helpers :as th]
|
|
[clojure.test :as t]
|
|
[yetti.request :as yreq]))
|
|
|
|
(defn- utf8-bytes
|
|
^bytes
|
|
[^String s]
|
|
(.getBytes s "UTF-8"))
|
|
|
|
;; --- DEFAULTS (ZERO ARGUMENTS)
|
|
|
|
(t/deftest make-dummy-request-zero-args-uses-defaults
|
|
(let [req (th/make-dummy-request)]
|
|
(t/is (instance? backend_tests.helpers.DummyRequest req))
|
|
(t/is (= :get (yreq/method req)))
|
|
(t/is (= "/test" (yreq/path req)))
|
|
(t/is (= :http (yreq/scheme req)))
|
|
(t/is (= "HTTP/1.1" (yreq/protocol req)))
|
|
(t/is (= "127.0.0.1" (yreq/remote-addr req)))
|
|
(t/is (= "test" (yreq/server-name req)))
|
|
(t/is (= 0 (yreq/server-port req)))
|
|
(t/is (nil? (yreq/query req)))
|
|
(t/is (nil? (yreq/body req)))))
|
|
|
|
;; --- SINGLE OPTIONS MAP
|
|
|
|
(t/deftest make-dummy-request-single-map-arg-overrides-given-keys
|
|
(let [req (th/make-dummy-request {:method :post
|
|
:headers {"x-test" "yes"}
|
|
:path "/custom"
|
|
:query "a=1"})]
|
|
(t/is (= :post (yreq/method req)))
|
|
(t/is (= "yes" (yreq/get-header req "x-test")))
|
|
(t/is (= "/custom" (yreq/path req)))
|
|
(t/is (= "a=1" (yreq/query req)))))
|
|
|
|
(t/deftest make-dummy-request-single-map-arg-keeps-defaults-for-omitted-keys
|
|
(let [req (th/make-dummy-request {:method :post})]
|
|
(t/is (= "/test" (yreq/path req)))
|
|
(t/is (= :http (yreq/scheme req)))
|
|
(t/is (= "test" (yreq/server-name req)))))
|
|
|
|
;; --- KEYWORD ARGUMENTS
|
|
|
|
(t/deftest make-dummy-request-keyword-args-override-given-keys
|
|
(let [req (th/make-dummy-request :method :delete
|
|
:path "/by-id"
|
|
:server-port 8080)]
|
|
(t/is (= :delete (yreq/method req)))
|
|
(t/is (= "/by-id" (yreq/path req)))
|
|
(t/is (= 8080 (yreq/server-port req)))))
|
|
|
|
;; --- BODY
|
|
|
|
(t/deftest make-dummy-request-string-body-bytes-wrapped-in-stream
|
|
(let [req (th/make-dummy-request :method :post :body-bytes "hello")]
|
|
(t/is (instance? java.io.ByteArrayInputStream (yreq/body req)))
|
|
(t/is (= "hello" (slurp (yreq/body req))))))
|
|
|
|
(t/deftest make-dummy-request-byte-array-body-bytes-wrapped-in-stream
|
|
(let [req (th/make-dummy-request :body-bytes (utf8-bytes "raw"))]
|
|
(t/is (= "raw" (slurp (yreq/body req))))))
|
|
|
|
(t/deftest make-dummy-request-body-stream-takes-precedence-over-body-bytes
|
|
(let [stream (java.io.ByteArrayInputStream. (utf8-bytes "stream"))
|
|
req (th/make-dummy-request :body-stream stream :body-bytes "bytes")]
|
|
(t/is (identical? stream (yreq/body req)))
|
|
(t/is (= "stream" (slurp (yreq/body req))))))
|
|
|
|
;; --- COOKIES
|
|
|
|
(t/deftest make-dummy-request-cookie-readable-via-get-cookie
|
|
(let [req (th/make-dummy-request {:cookies {"auth-token" "abc"}})]
|
|
(t/is (= {:value "abc"} (yreq/get-cookie req "auth-token")))))
|
|
|
|
(t/deftest make-dummy-request-missing-cookie-returns-nil-value
|
|
(t/is (= {:value nil} (yreq/get-cookie (th/make-dummy-request) "missing"))))
|
|
|
|
;; --- RPC PARAMS: MULTIPART FLAG
|
|
|
|
(t/deftest prepare-rpc-params-marks-multipart-request
|
|
(let [params (#'th/prepare-rpc-params
|
|
(with-meta {::th/type :dummy
|
|
:events [{:name "event"}]}
|
|
{:app.http/request {:headers {"content-type" "multipart/form-data; boundary=xxx"}}}))]
|
|
(t/is (true? (:app.rpc/is-multipart params)))))
|
|
|
|
(t/deftest prepare-rpc-params-leaves-non-multipart-request-unmarked
|
|
(let [params (#'th/prepare-rpc-params
|
|
(with-meta {::th/type :dummy
|
|
:events [{:name "event"}]}
|
|
{:app.http/request {:headers {"content-type" "application/transit+json"}}}))]
|
|
(t/is (false? (:app.rpc/is-multipart params)))))
|
|
|
|
(t/deftest prepare-rpc-params-leaves-missing-content-type-unmarked
|
|
(let [params (#'th/prepare-rpc-params {::th/type :dummy
|
|
:events [{:name "event"}]})]
|
|
(t/is (false? (:app.rpc/is-multipart params)))))
|
|
|
|
(t/deftest prepare-rpc-params-body-cannot-forge-multipart-flag
|
|
(let [params (#'th/prepare-rpc-params
|
|
(with-meta {::th/type :dummy
|
|
:app.rpc/is-multipart true
|
|
:events [{:name "event"}]}
|
|
{:app.http/request {:headers {"content-type" "application/json"}}}))]
|
|
(t/is (false? (:app.rpc/is-multipart params)))))
|
|
|
|
;; --- RPC PARAMS: REQUEST METADATA
|
|
|
|
(t/deftest prepare-rpc-params-preserves-supplied-request
|
|
(let [supplied {:headers {"x-frontend-version" "1.2.3"}
|
|
:app.http/auth-key-id :admin-console}
|
|
params (#'th/prepare-rpc-params
|
|
(with-meta {::th/type :dummy
|
|
:events [{:name "event"}]}
|
|
{:app.http/request supplied}))
|
|
request (:app.http/request (meta params))]
|
|
(t/is (= "1.2.3" (yreq/get-header request "x-frontend-version")))
|
|
(t/is (= :admin-console (:app.http/auth-key-id request)))
|
|
(t/is (= {:events [{:name "event"}]} (:params request)))))
|
|
|
|
(t/deftest prepare-rpc-params-builds-dummy-request-without-metadata
|
|
(let [params (#'th/prepare-rpc-params {::th/type :dummy
|
|
:events [{:name "event"}]})
|
|
request (:app.http/request (meta params))]
|
|
(t/is (instance? backend_tests.helpers.DummyRequest request))
|
|
(t/is (= {:events [{:name "event"}]} (:params request)))))
|
|
|
|
(t/deftest prepare-rpc-params-falls-back-to-dummy-for-non-map-request
|
|
(let [supplied (reify yreq/IRequest
|
|
(get-header [_ _] nil))
|
|
params (#'th/prepare-rpc-params
|
|
(with-meta {::th/type :dummy
|
|
:events [{:name "event"}]}
|
|
{:app.http/request supplied}))
|
|
request (:app.http/request (meta params))]
|
|
(t/is (instance? backend_tests.helpers.DummyRequest request))
|
|
(t/is (= {:events [{:name "event"}]} (:params request)))))
|