Add unit and integration tests
I won't be lying, Claude Code was extremely helpful to write those tests, although it had a lot of issues with properly using in-memory SQLite (db and integration tests) and with parsing responses (integration test).
This commit is contained in:
106
test/yohoho/db_test.clj
Normal file
106
test/yohoho/db_test.clj
Normal file
@@ -0,0 +1,106 @@
|
||||
(ns yohoho.db-test
|
||||
(:require
|
||||
[clojure.test :refer :all]
|
||||
[next.jdbc :as jdbc]
|
||||
[yohoho.db :as db]))
|
||||
|
||||
;; In-memory database setup ---------------------------------------
|
||||
(def schema-sql
|
||||
"CREATE TABLE items (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT NOT NULL
|
||||
);")
|
||||
|
||||
(defn with-memory-db [f]
|
||||
(let [conn (jdbc/get-connection
|
||||
{:dbtype "sqlite"
|
||||
:dbname ":memory:"})]
|
||||
;; create schema on the SAME connection
|
||||
(jdbc/execute! conn [schema-sql])
|
||||
|
||||
;; force db.clj to reuse this connection
|
||||
(with-redefs [db/db conn]
|
||||
(f))
|
||||
|
||||
(.close conn)))
|
||||
|
||||
(use-fixtures :each with-memory-db)
|
||||
|
||||
;; Tests ----------------------------------------------------------
|
||||
(deftest test-init
|
||||
(testing "Database initialization creates items table"
|
||||
;; Drop the table first to test initialization
|
||||
(try
|
||||
(jdbc/execute! db/db ["DROP TABLE IF EXISTS items"])
|
||||
(catch Exception _))
|
||||
(db/init!)
|
||||
;; Verify table exists by trying to query it
|
||||
(is (vector? (db/get-all-items)))))
|
||||
|
||||
|
||||
(deftest test-store-item
|
||||
(testing "Storing item returns an ID"
|
||||
(let [id (db/store-item! "Test Item" "test@example.com")]
|
||||
(is (number? id))
|
||||
(is (> id 0))))
|
||||
|
||||
(testing "Stored item can be retrieved"
|
||||
(let [id (db/store-item! "Test Item" "test@example.com")
|
||||
item (db/get-item id)]
|
||||
(is (= "Test Item" (:name item)))
|
||||
(is (= "test@example.com" (:email item)))
|
||||
(is (= id (:id item)))))
|
||||
|
||||
(testing "Multiple items get unique IDs"
|
||||
(let [id1 (db/store-item! "Item 1" "item1@example.com")
|
||||
id2 (db/store-item! "Item 2" "item2@example.com")]
|
||||
(is (not= id1 id2)))))
|
||||
|
||||
|
||||
(deftest test-get-item
|
||||
(testing "Getting non-existent item returns nil"
|
||||
(is (nil? (db/get-item 999))))
|
||||
|
||||
(testing "Getting existing item returns map with correct keys"
|
||||
(let [id (db/store-item! "Test" "test@example.com")
|
||||
item (db/get-item id)]
|
||||
(is (map? item))
|
||||
(is (contains? item :id))
|
||||
(is (contains? item :name))
|
||||
(is (contains? item :email))
|
||||
;; Keys should be unqualified
|
||||
(is (not (qualified-keyword? :id)))
|
||||
(is (not (qualified-keyword? :name)))
|
||||
(is (not (qualified-keyword? :email)))))
|
||||
|
||||
(testing "Retrieved item has correct values"
|
||||
(let [id (db/store-item! "My Item" "myitem@example.com")
|
||||
item (db/get-item id)]
|
||||
(is (= id (:id item)))
|
||||
(is (= "My Item" (:name item)))
|
||||
(is (= "myitem@example.com" (:email item))))))
|
||||
|
||||
|
||||
(deftest test-get-all-items
|
||||
(testing "Empty database returns empty vector"
|
||||
(is (= [] (db/get-all-items))))
|
||||
|
||||
(testing "Database with items returns all items"
|
||||
(db/store-item! "Item 1" "item1@example.com")
|
||||
(db/store-item! "Item 2" "item2@example.com")
|
||||
(db/store-item! "Item 3" "item3@example.com")
|
||||
(let [items (db/get-all-items)]
|
||||
(is (= 3 (count items)))
|
||||
(is (every? map? items))
|
||||
(is (every? #(contains? % :id) items))
|
||||
(is (every? #(contains? % :name) items))
|
||||
(is (every? #(contains? % :email) items))))
|
||||
|
||||
(testing "Items are returned with unqualified keys"
|
||||
(db/store-item! "Test" "test@example.com")
|
||||
(let [items (db/get-all-items)
|
||||
item (first items)]
|
||||
(is (not (qualified-keyword? :id)))
|
||||
(is (not (qualified-keyword? :name)))
|
||||
(is (not (qualified-keyword? :email))))))
|
||||
65
test/yohoho/handlers_test.clj
Normal file
65
test/yohoho/handlers_test.clj
Normal file
@@ -0,0 +1,65 @@
|
||||
(ns yohoho.handlers-test
|
||||
(:require [clojure.test :refer :all]
|
||||
[yohoho.handlers :as handler]
|
||||
[yohoho.db :as db]))
|
||||
|
||||
(deftest test-ahoy
|
||||
(testing "Health check endpoint returns 200"
|
||||
(let [response (handler/ahoy {})]
|
||||
(is (= 200 (:status response)))
|
||||
(is (string? (:body response)))
|
||||
(is (re-find #"Ahoy" (:body response))))))
|
||||
|
||||
(deftest test-get-items
|
||||
(testing "Getting all items returns proper structure"
|
||||
(with-redefs [db/get-all-items (fn [] [{:id 1 :name "Test" :email "test@example.org"}
|
||||
{:id 2 :name "Test2" :email "test2@example.org"}])]
|
||||
(let [response (handler/get-items {})]
|
||||
(is (= 200 (:status response)))
|
||||
(is (map? (:body response)))
|
||||
(is (vector? (get-in response [:body :items])))
|
||||
(is (= 2 (count (get-in response [:body :items])))))))
|
||||
|
||||
(testing "Getting items with empty database"
|
||||
(with-redefs [db/get-all-items (fn [] [])]
|
||||
(let [response (handler/get-items {})]
|
||||
(is (= 200 (:status response)))
|
||||
(is (= [] (get-in response [:body :items])))))))
|
||||
|
||||
(deftest test-get-item
|
||||
(testing "Getting existing item returns 200"
|
||||
(with-redefs [db/get-item (fn [id]
|
||||
(when (= id 1)
|
||||
{:id 1 :name "Test" :email "test@example.org"}))]
|
||||
(let [request {:path-params {:id 1}}
|
||||
response (handler/get-item request)]
|
||||
(is (= 200 (:status response)))
|
||||
(is (= {:id 1 :name "Test" :email "test@example.org"} (:body response))))))
|
||||
|
||||
(testing "Getting non-existent item returns 404"
|
||||
(with-redefs [db/get-item (fn [_] nil)]
|
||||
(let [request {:path-params {:id 999}}
|
||||
response (handler/get-item request)]
|
||||
(is (= 404 (:status response)))
|
||||
(is (= {:error "Not Found"} (:body response)))))))
|
||||
|
||||
(deftest test-create-item
|
||||
(testing "Creating item with valid data returns 201"
|
||||
(with-redefs [db/store-item! (fn [_name _email] 42)
|
||||
db/get-item (fn [id]
|
||||
(when (= id 42)
|
||||
{:id 42 :name "New Item" :email "new@example.org"}))]
|
||||
(let [request {:body-params {:name "New Item" :email "new@example.org"}}
|
||||
response (handler/create-item request)]
|
||||
(is (= 201 (:status response)))
|
||||
(is (= "/item/42" (get-in response [:headers "Location"])))
|
||||
(is (= {:id 42 :name "New Item" :email "new@example.org"} (:body response))))))
|
||||
|
||||
(testing "Created item has all expected fields"
|
||||
(with-redefs [db/store-item! (fn [_name _email] 1)
|
||||
db/get-item (fn [id] {:id id :name "Test" :email "test@example.org"})]
|
||||
(let [request {:body-params {:name "Test" :email "test@example.org"}}
|
||||
response (handler/create-item request)]
|
||||
(is (contains? (:body response) :id))
|
||||
(is (contains? (:body response) :name))
|
||||
(is (contains? (:body response) :email))))))
|
||||
35
test/yohoho/helpers_test.clj
Normal file
35
test/yohoho/helpers_test.clj
Normal file
@@ -0,0 +1,35 @@
|
||||
(ns yohoho.helpers-test
|
||||
(:require [clojure.test :refer :all]
|
||||
[yohoho.helpers :as helpers]))
|
||||
|
||||
(deftest test-post-payload-content-type-json-middleware
|
||||
(let [mock-handler (fn [_request] {:status 200 :body "Success"})
|
||||
wrapped-handler (helpers/post-payload-content-type-json-middleware mock-handler)]
|
||||
|
||||
(testing "GET requests pass through without content-type check"
|
||||
(let [request {:request-method :get}
|
||||
response (wrapped-handler request)]
|
||||
(is (= 200 (:status response)))
|
||||
(is (= "Success" (:body response)))))
|
||||
|
||||
(testing "POST with application/json passes through"
|
||||
(let [request {:request-method :post
|
||||
:headers {"content-type" "application/json"}}
|
||||
response (wrapped-handler request)]
|
||||
(is (= 200 (:status response)))
|
||||
(is (= "Success" (:body response)))))
|
||||
|
||||
(testing "POST without content-type returns 415"
|
||||
(let [request {:request-method :post
|
||||
:headers {}}
|
||||
response (wrapped-handler request)]
|
||||
(is (= 415 (:status response)))
|
||||
(is (= "Unsupported Media Type" (get-in response [:body :error])))
|
||||
(is (= "Content-Type must be application/json" (get-in response [:body :message])))))
|
||||
|
||||
(testing "POST with wrong content-type returns 415"
|
||||
(let [request {:request-method :post
|
||||
:headers {"content-type" "application/xml"}}
|
||||
response (wrapped-handler request)]
|
||||
(is (= 415 (:status response)))
|
||||
(is (= "Unsupported Media Type" (get-in response [:body :error])))))))
|
||||
196
test/yohoho/integration_test.clj
Normal file
196
test/yohoho/integration_test.clj
Normal file
@@ -0,0 +1,196 @@
|
||||
(ns yohoho.integration-test
|
||||
(:require [clojure.test :refer :all]
|
||||
[yohoho.app :as app]
|
||||
[yohoho.db :as db]
|
||||
[next.jdbc :as jdbc]
|
||||
[clojure.data.json :as json]))
|
||||
|
||||
|
||||
;; In-memory database setup ---------------------------------------
|
||||
(def schema-sql
|
||||
"CREATE TABLE items (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT NOT NULL
|
||||
);")
|
||||
|
||||
(defn with-memory-db [f]
|
||||
(let [conn (jdbc/get-connection
|
||||
{:dbtype "sqlite"
|
||||
:dbname ":memory:"})]
|
||||
;; create schema on the SAME connection
|
||||
(jdbc/execute! conn [schema-sql])
|
||||
|
||||
;; force db.clj to reuse this connection
|
||||
(with-redefs [db/db conn]
|
||||
(f))
|
||||
|
||||
(.close conn)))
|
||||
|
||||
(use-fixtures :each with-memory-db)
|
||||
|
||||
|
||||
;; Helpers --------------------------------------------------------
|
||||
(defn parse-json-body [response]
|
||||
(when-let [body (:body response)]
|
||||
(cond
|
||||
(string? body)
|
||||
(json/read-str body :key-fn keyword)
|
||||
|
||||
(instance? java.io.InputStream body)
|
||||
(json/read (java.io.InputStreamReader. body) :key-fn keyword)
|
||||
|
||||
:else
|
||||
body)))
|
||||
|
||||
(defn make-request
|
||||
"Helper to make a request to the app"
|
||||
[method uri & {:keys [body headers]}]
|
||||
(let [request {:request-method method
|
||||
:uri uri
|
||||
:headers (merge {"content-type" "application/json"} headers)}
|
||||
request (if body
|
||||
(assoc request :body-params body)
|
||||
request)]
|
||||
(app/app request)))
|
||||
|
||||
|
||||
;; Tests ----------------------------------------------------------
|
||||
(deftest test-health-check
|
||||
(testing "Health check endpoint returns 200"
|
||||
(let [response (make-request :get "/ahoy")]
|
||||
(is (= 200 (:status response)))
|
||||
(is (string? (:body response))))))
|
||||
|
||||
(deftest test-create-and-retrieve-item
|
||||
(testing "Creating an item and retrieving it"
|
||||
(let [item-data {:name "Integration Test Item" :email "integration@test.com"}
|
||||
create-response (make-request :post "/items" :body item-data)]
|
||||
|
||||
;; Verify creation response
|
||||
(is (= 201 (:status create-response)))
|
||||
(let [created-item (parse-json-body create-response)]
|
||||
(is (number? (:id created-item)))
|
||||
(is (= "Integration Test Item" (:name created-item)))
|
||||
(is (= "integration@test.com" (:email created-item)))
|
||||
|
||||
;; Verify Location header
|
||||
(is (= (str "/item/" (:id created-item))
|
||||
(get-in create-response [:headers "Location"])))
|
||||
|
||||
;; Retrieve the item by ID
|
||||
(let [get-response (make-request :get (str "/item/" (:id created-item)))
|
||||
retrieved-item (parse-json-body get-response)]
|
||||
(is (= 200 (:status get-response)))
|
||||
(is (= created-item retrieved-item)))))))
|
||||
|
||||
(deftest test-get-all-items
|
||||
(testing "Getting all items returns empty list initially"
|
||||
(let [response (make-request :get "/items")
|
||||
body (parse-json-body response)]
|
||||
(is (= 200 (:status response)))
|
||||
(is (= [] (:items body)))))
|
||||
|
||||
(testing "Getting all items after creating some"
|
||||
;; Create multiple items
|
||||
(make-request :post "/items" :body {:name "Item 1" :email "item1@test.com"})
|
||||
(make-request :post "/items" :body {:name "Item 2" :email "item2@test.com"})
|
||||
(make-request :post "/items" :body {:name "Item 3" :email "item3@test.com"})
|
||||
|
||||
(let [response (make-request :get "/items")
|
||||
body (parse-json-body response)]
|
||||
(is (= 200 (:status response)))
|
||||
(is (= 3 (count (:items body))))
|
||||
(is (every? #(contains? % :id) (:items body)))
|
||||
(is (every? #(contains? % :name) (:items body)))
|
||||
(is (every? #(contains? % :email) (:items body))))))
|
||||
|
||||
(deftest test-get-nonexistent-item
|
||||
(testing "Getting non-existent item returns 404"
|
||||
(let [response (make-request :get "/item/999")
|
||||
body (parse-json-body response)]
|
||||
(is (= 404 (:status response)))
|
||||
(is (= "Not Found" (:error body))))))
|
||||
|
||||
(deftest test-create-item-validation
|
||||
(testing "Creating item without email fails validation"
|
||||
(let [response (make-request :post "/items" :body {:name "Test"})]
|
||||
(is (= 400 (:status response)))))
|
||||
|
||||
(testing "Creating item without name fails validation"
|
||||
(let [response (make-request :post "/items" :body {:email "test@example.com"})]
|
||||
(is (= 400 (:status response)))))
|
||||
|
||||
(testing "Creating item with invalid email fails validation"
|
||||
(let [response (make-request :post "/items" :body {:name "Test" :email "not-an-email"})]
|
||||
(is (= 400 (:status response)))))
|
||||
|
||||
(testing "Creating item with empty payload fails validation"
|
||||
(let [response (make-request :post "/items" :body {})]
|
||||
(is (= 400 (:status response))))))
|
||||
|
||||
(deftest test-content-type-validation
|
||||
(testing "POST without content-type returns 415"
|
||||
(let [response (make-request :post "/items" :body {:name "Test" :email "test@test.com"}
|
||||
:headers {"content-type" nil})]
|
||||
(is (= 415 (:status response)))))
|
||||
|
||||
(testing "POST with wrong content-type returns 415"
|
||||
(let [response (make-request :post "/items" :body {:name "Test" :email "test@test.com"}
|
||||
:headers {"content-type" "application/xml"})]
|
||||
(is (= 415 (:status response))))))
|
||||
|
||||
(deftest test-invalid-routes
|
||||
(testing "Non-existent route returns 404"
|
||||
(let [response (make-request :get "/nonexistent")]
|
||||
(is (= 404 (:status response))))))
|
||||
|
||||
(deftest test-openapi-endpoint
|
||||
(testing "OpenAPI documentation is available"
|
||||
(let [response (make-request :get "/openapi.json")]
|
||||
(is (= 200 (:status response)))
|
||||
(let [openapi-doc (parse-json-body response)]
|
||||
(is (= "Yo-ho-ho API" (get-in openapi-doc [:info :title])))
|
||||
(is (contains? openapi-doc :paths))))))
|
||||
|
||||
(deftest test-item-id-coercion
|
||||
(testing "Item ID path parameter is coerced to integer"
|
||||
;; Create an item first
|
||||
(let [create-response (make-request :post "/items"
|
||||
:body {:name "Test" :email "test@test.com"})
|
||||
created-item (parse-json-body create-response)
|
||||
item-id (:id created-item)]
|
||||
|
||||
;; Request with string ID should work (coerced to int)
|
||||
(let [response (make-request :get (str "/item/" item-id))]
|
||||
(is (= 200 (:status response))))))
|
||||
|
||||
(testing "Invalid ID format returns 400"
|
||||
(let [response (make-request :get "/item/not-a-number")]
|
||||
(is (= 400 (:status response))))))
|
||||
|
||||
(deftest test-multiple-items-workflow
|
||||
(testing "Complete workflow: create multiple items, retrieve all, retrieve individual"
|
||||
(let [items-to-create [{:name "Alice" :email "alice@example.com"}
|
||||
{:name "Bob" :email "bob@example.com"}
|
||||
{:name "Charlie" :email "charlie@example.com"}]
|
||||
created-ids (atom [])]
|
||||
|
||||
;; Create items
|
||||
(doseq [item items-to-create]
|
||||
(let [response (make-request :post "/items" :body item)
|
||||
created (parse-json-body response)]
|
||||
(is (= 201 (:status response)))
|
||||
(swap! created-ids conj (:id created))))
|
||||
|
||||
;; Verify all items in list
|
||||
(let [list-response (make-request :get "/items")
|
||||
all-items (:items (parse-json-body list-response))]
|
||||
(is (= 3 (count all-items)))
|
||||
(is (= #{"Alice" "Bob" "Charlie"} (set (map :name all-items)))))
|
||||
|
||||
;; Verify each item individually
|
||||
(doseq [id @created-ids]
|
||||
(let [response (make-request :get (str "/item/" id))]
|
||||
(is (= 200 (:status response)))
|
||||
(is (= id (:id (parse-json-body response)))))))))
|
||||
97
test/yohoho/schemas_test.clj
Normal file
97
test/yohoho/schemas_test.clj
Normal file
@@ -0,0 +1,97 @@
|
||||
(ns yohoho.schemas-test
|
||||
(:require [clojure.test :refer :all]
|
||||
[yohoho.schemas :as s]
|
||||
[malli.core :as m]))
|
||||
|
||||
(deftest test-email-validation
|
||||
(testing "Valid email addresses"
|
||||
(is (m/validate s/Email "test@example.com"))
|
||||
(is (m/validate s/Email "user.name@example.com"))
|
||||
(is (m/validate s/Email "user+tag@example.co.uk"))
|
||||
(is (m/validate s/Email "user_123@test-domain.com"))
|
||||
(is (m/validate s/Email "a@b.c")))
|
||||
|
||||
(testing "Invalid email addresses"
|
||||
(is (not (m/validate s/Email "notanemail")))
|
||||
(is (not (m/validate s/Email "@example.com")))
|
||||
(is (not (m/validate s/Email "user@")))
|
||||
(is (not (m/validate s/Email "user @example.com")))
|
||||
(is (not (m/validate s/Email "user@example")))
|
||||
(is (not (m/validate s/Email "")))))
|
||||
|
||||
(deftest test-item-input-schema
|
||||
(testing "Valid item input"
|
||||
(is (m/validate s/ItemInput {:name "Test Item" :email "test@example.com"}))
|
||||
(is (m/validate s/ItemInput {:name "A" :email "a@b.c"})))
|
||||
|
||||
(testing "Invalid item input - missing fields"
|
||||
(is (not (m/validate s/ItemInput {:name "Test"})))
|
||||
(is (not (m/validate s/ItemInput {:email "test@example.com"})))
|
||||
(is (not (m/validate s/ItemInput {}))))
|
||||
|
||||
(testing "Invalid item input - wrong types"
|
||||
(is (not (m/validate s/ItemInput {:name 123 :email "test@example.com"})))
|
||||
(is (not (m/validate s/ItemInput {:name "Test" :email "not-an-email"}))))
|
||||
|
||||
(testing "Extra fields are allowed (open map)"
|
||||
(is (m/validate s/ItemInput {:name "Test" :email "test@example.com" :extra "field"}))))
|
||||
|
||||
(deftest test-item-schema
|
||||
(testing "Valid item"
|
||||
(is (m/validate s/Item {:id 1 :name "Test" :email "test@example.com"}))
|
||||
(is (m/validate s/Item {:id 999 :name "Long Name Here" :email "complex.email+tag@example.co.uk"})))
|
||||
|
||||
(testing "Invalid item - missing fields"
|
||||
(is (not (m/validate s/Item {:id 1 :name "Test"})))
|
||||
(is (not (m/validate s/Item {:id 1 :email "test@example.com"})))
|
||||
(is (not (m/validate s/Item {:name "Test" :email "test@example.com"}))))
|
||||
|
||||
(testing "Invalid item - wrong types"
|
||||
(is (not (m/validate s/Item {:id "not-a-number" :name "Test" :email "test@example.com"})))
|
||||
(is (not (m/validate s/Item {:id 1.5 :name "Test" :email "test@example.com"})))
|
||||
(is (not (m/validate s/Item {:id 1 :name "Test" :email "invalid-email"})))))
|
||||
|
||||
(deftest test-items-response-schema
|
||||
(testing "Valid items response"
|
||||
(is (m/validate s/ItemsResponse {:items []}))
|
||||
(is (m/validate s/ItemsResponse {:items [{:id 1 :name "Test" :email "test@example.com"}]}))
|
||||
(is (m/validate s/ItemsResponse {:items [{:id 1 :name "Test1" :email "test1@example.com"}
|
||||
{:id 2 :name "Test2" :email "test2@example.com"}]})))
|
||||
|
||||
(testing "Invalid items response - not a vector"
|
||||
(is (not (m/validate s/ItemsResponse {:items {:id 1 :name "Test" :email "test@example.com"}})))
|
||||
(is (not (m/validate s/ItemsResponse {:items "not-a-vector"}))))
|
||||
|
||||
(testing "Invalid items response - missing items key"
|
||||
(is (not (m/validate s/ItemsResponse {})))
|
||||
(is (not (m/validate s/ItemsResponse {:data []}))))
|
||||
|
||||
(testing "Invalid items response - invalid item in vector"
|
||||
(is (not (m/validate s/ItemsResponse {:items [{:id 1 :name "Test"}]})))))
|
||||
|
||||
(deftest test-error-response-schema
|
||||
(testing "Valid error response with message"
|
||||
(is (m/validate s/ErrorResponse {:error "Not Found" :message "Item does not exist"})))
|
||||
|
||||
(testing "Valid error response without message"
|
||||
(is (m/validate s/ErrorResponse {:error "Not Found"})))
|
||||
|
||||
(testing "Invalid error response - missing error"
|
||||
(is (not (m/validate s/ErrorResponse {:message "Something went wrong"})))
|
||||
(is (not (m/validate s/ErrorResponse {}))))
|
||||
|
||||
(testing "Invalid error response - wrong types"
|
||||
(is (not (m/validate s/ErrorResponse {:error 404 :message "Not Found"})))
|
||||
(is (not (m/validate s/ErrorResponse {:error "Not Found" :message 123})))))
|
||||
|
||||
(deftest test-error-400-response-schema
|
||||
(testing "Valid 400 error response"
|
||||
(is (m/validate s/Error400Response {:value {}
|
||||
:type "reitit.coercion/request-coercion"
|
||||
:coercion "malli"
|
||||
:in ["request" "body-params"]
|
||||
:humanized {:email ["should be a valid email"]}})))
|
||||
|
||||
(testing "Invalid 400 error - missing required fields"
|
||||
(is (not (m/validate s/Error400Response {:value {}})))
|
||||
(is (not (m/validate s/Error400Response {:type "type" :coercion "malli" :in [] :humanized {}})))))
|
||||
Reference in New Issue
Block a user