Skip to content

Commit f5c347b

Browse files
Add more tests
1 parent c821cae commit f5c347b

File tree

4 files changed

+210
-14
lines changed

4 files changed

+210
-14
lines changed

src/flamebin/web.clj

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,14 @@
9393
;; HTML
9494
["" {}
9595
["/" {:get {:handler #'$page-list-profiles}}]
96-
["/public/*path" {:get {:handler #'$public-resource}}]
97-
["/profiles/upload" {:get {:handler #'$page-upload-file}}]
9896
["/:profile-id" {:name ::profile-page
9997
:get {:handler #'$render-profile
10098
:coercion reitit.coercion.malli/coercion
10199
:parameters {:path {:profile-id :nano-id}
102-
:query' {:read-token :string}}}}]]
100+
:query' [:map
101+
[:read-token {:optional true} :string]]}}}]
102+
["/profiles/upload" {:get {:handler #'$page-upload-file}}]
103+
["/public/*path" {:get {:handler #'$public-resource}}]]
103104

104105
;; API
105106
["/api/v1" {}
@@ -111,11 +112,7 @@
111112
:get {:handler #'$delete-profile
112113
:parameters {:query' {:id :nano-id
113114
:edit-token :string}}}}]
114-
#_["/profiles" {:get {:handler #'$list-profiles}}]]
115-
116-
;; Infra
117-
["/infra" {}
118-
["/health" {:get {:handler (fn [_] (resp (str (java.util.Date.))))}}]]]
115+
#_["/profiles" {:get {:handler #'$list-profiles}}]]]
119116
{:data {:middleware [parameters-middleware
120117
;; Needed for coercion to work.
121118
ring-coercion/coerce-exceptions-middleware

test/flamebin/test_utils.clj

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
(ns flamebin.test-utils
2-
(:require [omniconf.core :as cfg]
3-
[taoensso.timbre :as log]
4-
[clojure.test.check.generators :as gen]))
2+
(:require [clojure.java.shell :as sh]
3+
[flamebin.config :as config]
4+
[flamebin.rate-limiter :as rl]
5+
[flamebin.web :as web]
6+
[mount.lite :as mount]
7+
[omniconf.core :as cfg]))
58

69
(defmacro with-config-redefs [bindings & body]
710
(let [bindings (partition 2 bindings)]
@@ -12,3 +15,50 @@
1215
~@body
1316
(finally
1417
(dorun (map cfg/set ~(mapv first bindings) old-vals#)))))))
18+
19+
(defmacro with-temp-db [& body]
20+
`(let [f# (java.io.File/createTempFile "test-db" ".db")]
21+
(try (with-config-redefs [[:db :path] (str f#)]
22+
~@body)
23+
(finally (.delete f#)))))
24+
25+
(defmacro with-temp-storage [& body]
26+
`(let [dir# (.toFile
27+
(java.nio.file.Files/createTempDirectory
28+
"storage" (into-array java.nio.file.attribute.FileAttribute [])))]
29+
(try (with-config-redefs [[:storage :path] dir#]
30+
~@body)
31+
(finally (sh/sh "rm" "-rf" (str dir#))))))
32+
33+
(defmacro with-temp-no-ratelimits [& body]
34+
`(binding [mount/*substitutes*
35+
(merge
36+
mount/*substitutes*
37+
{#'rl/global-processed-kbytes-limiter (mount/state :start (constantly true))
38+
#'rl/global-saved-kbytes-limiter (mount/state :start (constantly true))
39+
#'rl/per-ip-limiters (mount/state :start (delay (constantly {:processed (constantly true)
40+
:saved (constantly true)})))})]
41+
~@body))
42+
43+
(defmacro with-temp-mount [& body]
44+
`(binding [config/*silent* true]
45+
(try (mount/stop)
46+
(println "CONFIG" (omniconf.core/get))
47+
(binding [mount/*substitutes* (merge mount/*substitutes*
48+
{#'web/prom-app {:start (constantly true)}})]
49+
(println "Started mount states:" (mount/start))
50+
(println " __ RL STATES:" @rl/global-processed-kbytes-limiter
51+
@rl/global-saved-kbytes-limiter
52+
@@#'rl/per-ip-limiters )
53+
)
54+
~@body
55+
(finally
56+
(mount/stop)
57+
(binding [mount/*substitutes* {}]
58+
(mount/start))))))
59+
60+
(defmacro with-temp [whats & body]
61+
(first
62+
(reduce (fn [body what]
63+
(list (cons (symbol (str "flamebin.test-utils/with-temp-" (name what))) body)))
64+
body (if (= whats :all) [:mount :db :storage :no-ratelimits] whats))))

test/flamebin/web_test.clj

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
(ns flamebin.web-test
2+
(:require [flamebin.web :as sut]
3+
[clojure.test :refer :all]
4+
[flamebin.util :refer [valid-id?]]
5+
[clojure.java.io :as io]
6+
[clojure.walk :as walk]
7+
[org.httpkit.client :as http]
8+
[flamebin.processing :as proc]
9+
[flamebin.test-utils :refer :all]
10+
[hickory.core :as html]
11+
[jsonista.core :as json]
12+
[matcher-combinators.test]
13+
[matcher-combinators.matchers :as matchers :refer [via]]
14+
)
15+
(:import java.time.Instant))
16+
17+
(defn- url [url] (str "http://localhost:8086" url))
18+
19+
(defn- req
20+
([type method url']
21+
(req type method url' {}))
22+
([type method url' opts]
23+
(let [resp (update @(http/request (merge opts {:method method :url (url url')}))
24+
:opts #(if (:body %)
25+
(assoc % :body "<redacted>")
26+
%))]
27+
(if (and (< (:status resp 999) 400) (#{:api :page} type))
28+
(case type
29+
:api (update resp :body #(json/read-value % (json/object-mapper {:decode-key-fn true})))
30+
:page (update resp :body #(html/as-hiccup (html/parse (if (string? %) % (some-> % slurp))))))
31+
(update resp :body #(if (string? %) % (some-> % slurp)))))))
32+
33+
(defn- find-elem
34+
([html tag] (find-elem html tag nil))
35+
([html tag id]
36+
(let [res (volatile! nil)]
37+
(walk/prewalk #(if (and (vector? %) (= (first %) tag)
38+
(or (nil? id) (= (:id (second %)) id)))
39+
(do (vreset! res %) nil)
40+
%)
41+
html)
42+
@res)))
43+
44+
(defn- gzip-content [content]
45+
(let [baos (java.io.ByteArrayOutputStream.)]
46+
(with-open [s (java.util.zip.GZIPOutputStream. baos)]
47+
(io/copy content s))
48+
(.toByteArray baos)))
49+
50+
#_(gzip-content (io/file "test/res/small.txt"))
51+
52+
(defn- serialized-edn [edn]
53+
(binding [*print-length* nil
54+
*print-level* nil]
55+
(.getBytes (pr-str edn))))
56+
57+
;; TODO: test unprocessable entity
58+
59+
(deftest basic-usage-test
60+
(with-temp :all
61+
(testing "open empty index page"
62+
(is (match? {:status 200
63+
:body (via #(find-elem % :ul "flamegraph-list")
64+
[:ul {:id "flamegraph-list"}])}
65+
(req :page :get "/"))))
66+
67+
(testing "upload test flamegraph"
68+
(let [resp (req :api :post "/api/v1/upload-profile?format=collapsed&type=cpu" {:body (io/file "test/res/small.txt")})]
69+
(is (match? {:status 201
70+
:headers {:x-read-token string?
71+
:x-edit-token string?
72+
:x-created-id valid-id?
73+
:location string?}
74+
:body {:upload_ts #(instance? Instant (Instant/parse %))
75+
:read-token string?
76+
:edit_token string?
77+
:is_public false
78+
:profile_type "cpu"
79+
:id valid-id?
80+
:file_path string?
81+
:owner "127.0.0.1"
82+
:sample_count 10050}}
83+
resp))
84+
85+
(testing "view flamegraph"
86+
(is (match? {:status 200
87+
:headers {:content-length (via parse-long #(> % 50000))}}
88+
(req nil :get (format "/%s?read-token=%s" (:id (:body resp)) (:read-token (:body resp)))))))
89+
90+
(testing "delete flamegraph"
91+
(let [resp3 (req :api :get (format "/api/v1/delete-profile?id=%s&edit-token=%s" (:id (:body resp)) (:edit_token (:body resp))))]
92+
(is (match? {:status 200
93+
:body {:message #"^Successfully deleted profile"}}
94+
resp3))))
95+
96+
(testing "index is still empty"
97+
(is (match? {:status 200
98+
:body (via #(find-elem % :ul "flamegraph-list")
99+
[:ul {:id "flamegraph-list"}])}
100+
(req :page :get "/"))))))))
101+
102+
(deftest front-page-visibility-test
103+
(with-temp :all
104+
(dotimes [_ 5]
105+
;; Upload one public and one private on each iteration
106+
(is (match? {:status 201}
107+
(req :api :post "/api/v1/upload-profile?format=collapsed&type=cpu&public=true"
108+
{:body (io/file "test/res/small.txt")})))
109+
(is (match? {:status 201}
110+
(req :api :post "/api/v1/upload-profile?format=collapsed&type=cpu"
111+
{:body (io/file "test/res/small.txt")}))))
112+
113+
(testing "front page should only show five public flamegraph"
114+
(let [front-page (req :page :get "/")
115+
flamegraph-list (-> front-page :body (find-elem :ul "flamegraph-list"))
116+
url-no-read-token (via #(-> (find-elem % :a) second :href)
117+
#"^/\w+$")]
118+
(is (match? {:status 200
119+
:body (via #(find-elem % :ul "flamegraph-list")
120+
(into [:ul {:id "flamegraph-list"}]
121+
(repeat 5 url-no-read-token)))}
122+
(req :page :get "/")))
123+
(testing "links on the frontpage lead to flamegraphs"
124+
(let [u (-> (find-elem flamegraph-list :a) second :href)]
125+
(is (match? {:status 200
126+
:headers {:content-length (via parse-long #(> % 50000))}}
127+
(req nil :get u)))))))))
128+
129+
(deftest different-upload-formats-test
130+
(with-temp :all
131+
(doseq [file ["small.txt" "normal.txt" "huge.txt"]
132+
frmt [:collapsed :dense-edn]
133+
gzip? [false true]
134+
;; Exclusions
135+
:when (not (or (= [file frmt gzip?] ["normal.txt" :collapsed false])
136+
(= [file frmt] ["huge.txt" :collapsed])))
137+
:let [file (io/file "test/res" file)]]
138+
(testing (format "upload: file=%s gzip?=%s format=%s" (str file) gzip? frmt)
139+
(let [resp (req :api :post (format "/api/v1/upload-profile?format=%s&type=cpu&public=true"
140+
(name frmt))
141+
{:headers (if gzip? {"Content-encoding" "gzip"} nil)
142+
:body (cond-> file
143+
(= frmt :dense-edn) (-> proc/collapsed-stacks-stream->dense-profile
144+
serialized-edn)
145+
gzip? gzip-content)})]
146+
(is (match? {:status 201} (dissoc resp :opts)))
147+
(is (match? {:status 200} (req :nil :get (str "/" (:id (:body resp)))))))))
148+
149+
(testing "big files are rejected by the webserver"
150+
(is (match? {:error any?}
151+
(req :api :post "/api/v1/upload-profile?format=collapsed&type=cpu&public=true"
152+
{:body (io/file "test/res/huge.txt")}))))))

test/res/huge.json

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)