|
| 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")})))))) |
0 commit comments