Add compatibility with hato and clj-http-lite insecure feature (#1080)
Added classes: - java.net.CookiePolicy - java.net.http.HttpTimeoutException - javax.net.ssl.HostnameVerifier - javax.net.ssl.HttpsURLConnection - javax.net.ssl.KeyManagerFactory - javax.net.ssl.SSLSession - javax.net.ssl.TrustManagerFactory - java.security.KeyStore - java.util.zip.Inflater - java.util.zip.ZipException
This commit is contained in:
parent
a50930fc14
commit
4566905a5c
9 changed files with 514 additions and 106 deletions
9
deps.edn
9
deps.edn
|
|
@ -51,11 +51,9 @@
|
|||
:main-opts ["-m" "babashka.profile"]}
|
||||
:lib-tests
|
||||
{:extra-paths ["process/src" "process/test" "test-resources/lib_tests"]
|
||||
:extra-deps {babashka/clj-http-lite
|
||||
{:git/url "https://github.com/babashka/clj-http-lite"
|
||||
:sha "f44ebe45446f0f44f2b73761d102af3da6d0a13e"}
|
||||
:extra-deps {org.clj-commons/clj-http-lite {:mvn/version "0.4.392"}
|
||||
borkdude/spartan.spec {:git/url "https://github.com/borkdude/spartan.spec"
|
||||
:sha "16f7eec4b6589c77c96c9fcf989f78fffcee7c4c"}
|
||||
:sha "7689f85d4c79de6c7b5ee3ab62e3e72eeccd3318"}
|
||||
lambdaisland/regal {:git/url "https://github.com/lambdaisland/regal"
|
||||
:sha "f902d2c43121f9e1c48603d6eb99f5900eb6a9f6"}
|
||||
weavejester/medley {:git/url "https://github.com/weavejester/medley"
|
||||
|
|
@ -92,7 +90,8 @@
|
|||
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
|
||||
com.stuartsierra/component {:mvn/version "1.0.0"}
|
||||
org.clojars.askonomm/ruuter {:mvn/version "1.2.2"}
|
||||
org.clj-commons/digest {:mvn/version "1.4.100"}}
|
||||
org.clj-commons/digest {:mvn/version "1.4.100"}
|
||||
hato/hato {:mvn/version "0.8.2"}}
|
||||
:classpath-overrides {org.clojure/clojure nil
|
||||
org.clojure/spec.alpha nil
|
||||
org.clojure/core.specs.alpha nil}}
|
||||
|
|
|
|||
2
sci
2
sci
|
|
@ -1 +1 @@
|
|||
Subproject commit 3b62462112bde39d166914d0e424075ed18195cf
|
||||
Subproject commit f3d2764b8f9fc14026c89e23d94fa2071ededcea
|
||||
|
|
@ -2,87 +2,92 @@
|
|||
{:no-doc true}
|
||||
(:require
|
||||
[babashka.impl.features :as features]
|
||||
[babashka.impl.proxy :as proxy]
|
||||
[cheshire.core :as json]
|
||||
[sci.impl.types :as t]))
|
||||
|
||||
(def base-custom-map
|
||||
`{clojure.lang.LineNumberingPushbackReader {:allPublicConstructors true
|
||||
:allPublicMethods true}
|
||||
java.lang.Thread
|
||||
{:allPublicConstructors true
|
||||
;; generated with `public-declared-method-names`, see in
|
||||
;; `comment` below
|
||||
:methods [{:name "activeCount"}
|
||||
{:name "checkAccess"}
|
||||
{:name "currentThread"}
|
||||
{:name "dumpStack"}
|
||||
{:name "enumerate"}
|
||||
{:name "getAllStackTraces"}
|
||||
{:name "getContextClassLoader"}
|
||||
{:name "getDefaultUncaughtExceptionHandler"}
|
||||
{:name "getId"}
|
||||
{:name "getName"}
|
||||
{:name "getPriority"}
|
||||
{:name "getStackTrace"}
|
||||
{:name "getState"}
|
||||
{:name "getThreadGroup"}
|
||||
{:name "getUncaughtExceptionHandler"}
|
||||
{:name "holdsLock"}
|
||||
{:name "interrupt"}
|
||||
{:name "interrupted"}
|
||||
{:name "isAlive"}
|
||||
{:name "isDaemon"}
|
||||
{:name "isInterrupted"}
|
||||
{:name "join"}
|
||||
{:name "run"}
|
||||
{:name "setContextClassLoader"}
|
||||
{:name "setDaemon"}
|
||||
{:name "setDefaultUncaughtExceptionHandler"}
|
||||
{:name "setName"}
|
||||
{:name "setPriority"}
|
||||
{:name "setUncaughtExceptionHandler"}
|
||||
{:name "sleep"}
|
||||
{:name "start"}
|
||||
{:name "toString"}
|
||||
{:name "yield"}]}
|
||||
java.net.URL
|
||||
{:allPublicConstructors true
|
||||
:allPublicFields true
|
||||
;; generated with `public-declared-method-names`, see in
|
||||
;; `comment` below
|
||||
:methods [{:name "equals"}
|
||||
{:name "getAuthority"}
|
||||
{:name "getContent"}
|
||||
{:name "getDefaultPort"}
|
||||
{:name "getFile"}
|
||||
{:name "getHost"}
|
||||
{:name "getPath"}
|
||||
{:name "getPort"}
|
||||
{:name "getProtocol"}
|
||||
{:name "getQuery"}
|
||||
{:name "getRef"}
|
||||
{:name "getUserInfo"}
|
||||
{:name "hashCode"}
|
||||
{:name "openConnection"}
|
||||
{:name "openStream"}
|
||||
{:name "sameFile"}
|
||||
;; not supported: {:name "setURLStreamHandlerFactory"}
|
||||
{:name "toExternalForm"}
|
||||
{:name "toString"}
|
||||
{:name "toURI"}]}
|
||||
java.util.Arrays
|
||||
{:methods [{:name "copyOf"}
|
||||
{:name "copyOfRange"}]}
|
||||
;; this fixes clojure.lang.Reflector for Java 11
|
||||
java.lang.reflect.AccessibleObject
|
||||
{:methods [{:name "canAccess"}]}
|
||||
java.lang.reflect.Method
|
||||
{:methods [{:name "getName"}]}
|
||||
java.net.Inet4Address
|
||||
{:methods [{:name "getHostAddress"}]}
|
||||
java.net.Inet6Address
|
||||
{:methods [{:name "getHostAddress"}]}})
|
||||
|
||||
(def custom-map
|
||||
(cond->
|
||||
`{clojure.lang.LineNumberingPushbackReader {:allPublicConstructors true
|
||||
:allPublicMethods true}
|
||||
java.lang.Thread
|
||||
{:allPublicConstructors true
|
||||
;; generated with `public-declared-method-names`, see in
|
||||
;; `comment` below
|
||||
:methods [{:name "activeCount"}
|
||||
{:name "checkAccess"}
|
||||
{:name "currentThread"}
|
||||
{:name "dumpStack"}
|
||||
{:name "enumerate"}
|
||||
{:name "getAllStackTraces"}
|
||||
{:name "getContextClassLoader"}
|
||||
{:name "getDefaultUncaughtExceptionHandler"}
|
||||
{:name "getId"}
|
||||
{:name "getName"}
|
||||
{:name "getPriority"}
|
||||
{:name "getStackTrace"}
|
||||
{:name "getState"}
|
||||
{:name "getThreadGroup"}
|
||||
{:name "getUncaughtExceptionHandler"}
|
||||
{:name "holdsLock"}
|
||||
{:name "interrupt"}
|
||||
{:name "interrupted"}
|
||||
{:name "isAlive"}
|
||||
{:name "isDaemon"}
|
||||
{:name "isInterrupted"}
|
||||
{:name "join"}
|
||||
{:name "run"}
|
||||
{:name "setContextClassLoader"}
|
||||
{:name "setDaemon"}
|
||||
{:name "setDefaultUncaughtExceptionHandler"}
|
||||
{:name "setName"}
|
||||
{:name "setPriority"}
|
||||
{:name "setUncaughtExceptionHandler"}
|
||||
{:name "sleep"}
|
||||
{:name "start"}
|
||||
{:name "toString"}
|
||||
{:name "yield"}]}
|
||||
java.net.URL
|
||||
{:allPublicConstructors true
|
||||
:allPublicFields true
|
||||
;; generated with `public-declared-method-names`, see in
|
||||
;; `comment` below
|
||||
:methods [{:name "equals"}
|
||||
{:name "getAuthority"}
|
||||
{:name "getContent"}
|
||||
{:name "getDefaultPort"}
|
||||
{:name "getFile"}
|
||||
{:name "getHost"}
|
||||
{:name "getPath"}
|
||||
{:name "getPort"}
|
||||
{:name "getProtocol"}
|
||||
{:name "getQuery"}
|
||||
{:name "getRef"}
|
||||
{:name "getUserInfo"}
|
||||
{:name "hashCode"}
|
||||
{:name "openConnection"}
|
||||
{:name "openStream"}
|
||||
{:name "sameFile"}
|
||||
;; not supported: {:name "setURLStreamHandlerFactory"}
|
||||
{:name "toExternalForm"}
|
||||
{:name "toString"}
|
||||
{:name "toURI"}]}
|
||||
java.util.Arrays
|
||||
{:methods [{:name "copyOf"}
|
||||
{:name "copyOfRange"}]}
|
||||
;; this fixes clojure.lang.Reflector for Java 11
|
||||
java.lang.reflect.AccessibleObject
|
||||
{:methods [{:name "canAccess"}]}
|
||||
java.lang.reflect.Method
|
||||
{:methods [{:name "getName"}]}
|
||||
java.net.Inet4Address
|
||||
{:methods [{:name "getHostAddress"}]}
|
||||
java.net.Inet6Address
|
||||
{:methods [{:name "getHostAddress"}]}}
|
||||
(merge base-custom-map
|
||||
proxy/custom-reflect-map)
|
||||
features/hsqldb? (assoc `org.hsqldb.dbinfo.DatabaseInformationFull
|
||||
{:methods [{:name "<init>"
|
||||
:parameterTypes ["org.hsqldb.Database"]}]}
|
||||
|
|
@ -189,6 +194,7 @@
|
|||
java.net.CookieHandler
|
||||
java.net.CookieManager
|
||||
java.net.CookieStore
|
||||
java.net.CookiePolicy
|
||||
java.net.HttpCookie
|
||||
java.net.PasswordAuthentication
|
||||
java.net.ProxySelector
|
||||
|
|
@ -204,15 +210,21 @@
|
|||
java.net.http.HttpResponse
|
||||
java.net.http.HttpResponse$BodyHandler
|
||||
java.net.http.HttpResponse$BodyHandlers
|
||||
java.net.http.HttpTimeoutException
|
||||
java.net.http.WebSocket
|
||||
java.net.http.WebSocket$Builder
|
||||
java.net.http.WebSocket$Listener
|
||||
java.security.cert.X509Certificate
|
||||
javax.crypto.Mac
|
||||
javax.crypto.spec.SecretKeySpec
|
||||
javax.net.ssl.HostnameVerifier ;; clj-http-lite
|
||||
javax.net.ssl.HttpsURLConnection ;; clj-http-lite
|
||||
javax.net.ssl.KeyManagerFactory
|
||||
javax.net.ssl.SSLContext
|
||||
javax.net.ssl.SSLParameters
|
||||
javax.net.ssl.SSLSession ;; clj-http-lite
|
||||
javax.net.ssl.TrustManager
|
||||
javax.net.ssl.TrustManagerFactory
|
||||
javax.net.ssl.X509TrustManager
|
||||
jdk.internal.net.http.HttpClientBuilderImpl
|
||||
jdk.internal.net.http.HttpClientFacade
|
||||
|
|
@ -259,6 +271,7 @@
|
|||
java.security.MessageDigest
|
||||
java.security.DigestInputStream
|
||||
java.security.Provider
|
||||
java.security.KeyStore
|
||||
java.security.SecureRandom
|
||||
java.security.Security
|
||||
java.sql.Date
|
||||
|
|
@ -337,6 +350,7 @@
|
|||
java.util.concurrent.TimeUnit
|
||||
java.util.function.Function
|
||||
java.util.function.Supplier
|
||||
java.util.zip.Inflater
|
||||
java.util.zip.InflaterInputStream
|
||||
java.util.zip.DeflaterInputStream
|
||||
java.util.zip.GZIPInputStream
|
||||
|
|
@ -344,6 +358,7 @@
|
|||
java.util.zip.ZipInputStream
|
||||
java.util.zip.ZipOutputStream
|
||||
java.util.zip.ZipEntry
|
||||
java.util.zip.ZipException
|
||||
java.util.zip.ZipFile
|
||||
~(symbol "[B")
|
||||
~(symbol "[I")
|
||||
|
|
|
|||
|
|
@ -61,9 +61,32 @@
|
|||
|
||||
["java.net.Authenticator" #{}]
|
||||
(proxy [java.net.Authenticator] []
|
||||
(getPasswordAuthentication [] ((method-or-bust methods 'getPasswordAuthentication) this)))
|
||||
(getPasswordAuthentication []
|
||||
((method-or-bust methods 'getPasswordAuthentication) this)))
|
||||
|
||||
["java.net.ProxySelector" #{}]
|
||||
(proxy [java.net.ProxySelector] []
|
||||
(connectFailed [_ _ _] ((method-or-bust methods 'connectFailed) this))
|
||||
(select [_ _] ((method-or-bust methods 'select) this))))))
|
||||
(connectFailed [uri socket-address ex]
|
||||
((method-or-bust methods 'connectFailed) this uri socket-address ex))
|
||||
(select [uri] ((method-or-bust methods 'select) this uri)))
|
||||
|
||||
["javax.net.ssl.HostnameVerifier" #{}]
|
||||
(proxy [javax.net.ssl.HostnameVerifier] []
|
||||
(verify [host-name session] ((method-or-bust methods 'verify) this host-name session))))))
|
||||
|
||||
(defn class-sym [c] (symbol (class-name c)))
|
||||
|
||||
(def custom-reflect-map
|
||||
{(class-sym (class (proxy-fn {:class java.net.Authenticator})))
|
||||
{:methods [{:name "getPasswordAuthentication"}]}
|
||||
(class-sym (class (proxy-fn {:class java.net.ProxySelector})))
|
||||
{:methods [{:name "connectFailed"}
|
||||
{:name "select"}]}
|
||||
(class-sym (class (proxy-fn {:class javax.net.ssl.HostnameVerifier})))
|
||||
{:methods [{:name "verify"}]}})
|
||||
|
||||
;;; Scratch
|
||||
|
||||
(comment
|
||||
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
(ns babashka.run-all-libtests
|
||||
(:require [clojure.java.io :as io]
|
||||
[clojure.string :as str]
|
||||
[clojure.test :as t]))
|
||||
[clojure.test :as t]
|
||||
[spartan.spec]))
|
||||
|
||||
(require 'clojure.spec.alpha)
|
||||
|
||||
(def ns-args (set (map symbol *command-line-args*)))
|
||||
|
||||
|
|
@ -260,6 +263,8 @@
|
|||
|
||||
(test-namespaces 'clj-commons.digest-test)
|
||||
|
||||
(test-namespaces 'hato.client-test)
|
||||
|
||||
;;;; final exit code
|
||||
|
||||
(let [{:keys [:test :fail :error] :as m} @status]
|
||||
|
|
|
|||
|
|
@ -1,25 +1,34 @@
|
|||
(ns clj-http.lite.client-test
|
||||
(:require [cheshire.core :as json]
|
||||
[clj-http.lite.client :as client]
|
||||
[clojure.test :as t :refer [deftest]]))
|
||||
[clojure.test :as t :refer [deftest is]]))
|
||||
|
||||
(deftest client-test
|
||||
(prn (:status (client/get "https://www.clojure.org" {:throw-exceptions false})))
|
||||
(is (= 200 (:status (client/get "https://www.clojure.org" {:throw-exceptions false}))))
|
||||
|
||||
(prn (:status (client/get "https://postman-echo.com/get?foo1=bar1&foo2=bar2" {:throw-exceptions false})))
|
||||
(is (= 200 (:status (client/get "https://postman-echo.com/get?foo1=bar1&foo2=bar2" {:throw-exceptions false}))))
|
||||
|
||||
(prn (:status (client/post "https://postman-echo.com/post" {:throw-exceptions false})))
|
||||
(is (= 200 (:status (client/post "https://postman-echo.com/post" {:throw-exceptions false}))))
|
||||
|
||||
(prn (:status (client/post "https://postman-echo.com/post"
|
||||
{:body (json/generate-string {:a 1})
|
||||
:headers {"X-Hasura-Role" "admin"}
|
||||
:content-type :json
|
||||
:accept :json
|
||||
:throw-exceptions false})))
|
||||
(is (= 200 (:status (client/post "https://postman-echo.com/post"
|
||||
{:body (json/generate-string {:a 1})
|
||||
:headers {"X-Hasura-Role" "admin"}
|
||||
:content-type :json
|
||||
:accept :json
|
||||
:throw-exceptions false}))))
|
||||
|
||||
(prn (:status (client/put "https://postman-echo.com/put"
|
||||
{:body (json/generate-string {:a 1})
|
||||
:headers {"X-Hasura-Role" "admin"}
|
||||
:content-type :json
|
||||
:accept :json
|
||||
:throw-exceptions false}))))
|
||||
(is (= 200 (:status (client/put "https://postman-echo.com/put"
|
||||
{:body (json/generate-string {:a 1})
|
||||
:headers {"X-Hasura-Role" "admin"}
|
||||
:content-type :json
|
||||
:accept :json
|
||||
:throw-exceptions false})))))
|
||||
|
||||
(deftest insecure-test
|
||||
(is (= 200 (:status (client/get "https://self-signed.badssl.com/" {:insecure? true})))))
|
||||
|
||||
(deftest exception-test
|
||||
(try (client/get "https://site.com/broken")
|
||||
(is false "should not reach here")
|
||||
(catch Exception e
|
||||
(is (:headers (ex-data e))))))
|
||||
|
|
|
|||
348
test-resources/lib_tests/hato/client_test.clj
Normal file
348
test-resources/lib_tests/hato/client_test.clj
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
(ns hato.client-test
|
||||
(:refer-clojure :exclude [get])
|
||||
(:require [clojure.test :refer :all]
|
||||
[hato.client :refer :all]
|
||||
[clojure.java.io :as io]
|
||||
[org.httpkit.server :as http-kit]
|
||||
[cheshire.core :as json]
|
||||
[cognitect.transit :as transit]
|
||||
#_[promesa.exec :as pexec]
|
||||
#_[ring.middleware.multipart-params])
|
||||
(:import (java.io InputStream ByteArrayOutputStream)
|
||||
(java.net ProxySelector CookieHandler CookieManager)
|
||||
(java.net.http HttpClient$Redirect HttpClient$Version HttpClient)
|
||||
(java.time Duration)
|
||||
(javax.net.ssl SSLContext)
|
||||
(java.util UUID)))
|
||||
|
||||
(defmacro with-server
|
||||
"Spins up a local HTTP server with http-kit."
|
||||
[handler & body]
|
||||
`(let [s# (http-kit/run-server ~handler {:port 1234})]
|
||||
(try ~@body
|
||||
(finally
|
||||
(s# :timeout 100)))))
|
||||
|
||||
(deftest test-build-http-client
|
||||
(testing "authenticator"
|
||||
(is (.isEmpty (.authenticator (build-http-client {}))) "not set by default")
|
||||
(is (= "user" (-> (build-http-client {:authenticator {:user "user" :pass "pass"}})
|
||||
.authenticator .get .getPasswordAuthentication .getUserName)))
|
||||
(is (.isEmpty (.authenticator (build-http-client {:authenticator :some-invalid-value}))) "ignore invalid input"))
|
||||
|
||||
(testing "connect-timeout"
|
||||
(is (.isEmpty (.connectTimeout (build-http-client {}))) "not set by default")
|
||||
(is (= 5 (-> (build-http-client {:connect-timeout 5}) (.connectTimeout) ^Duration (.get) (.toMillis))))
|
||||
(is (thrown? Exception (build-http-client {:connect-timeout :not-a-number}))))
|
||||
|
||||
(testing "cookie-manager and cookie-policy"
|
||||
(is (.isEmpty (.cookieHandler (build-http-client {}))) "not set by default")
|
||||
(are [x] (instance? CookieHandler (-> ^HttpClient (build-http-client {:cookie-policy x}) (.cookieHandler) (.get)))
|
||||
:none
|
||||
:all
|
||||
:original-server
|
||||
:any-random-thing ; Invalid values are ignored, so the default :original-server will be in effect
|
||||
)
|
||||
|
||||
(let [cm (CookieManager.)]
|
||||
(is (= cm (-> (build-http-client {:cookie-handler cm :cookie-policy :all}) (.cookieHandler) (.get)))
|
||||
":cookie-handler takes precedence over :cookie-policy")))
|
||||
|
||||
(testing "redirect-policy"
|
||||
(is (= HttpClient$Redirect/NEVER (.followRedirects (build-http-client {}))) "NEVER by default")
|
||||
(are [expected option] (= expected (.followRedirects (build-http-client {:redirect-policy option})))
|
||||
HttpClient$Redirect/ALWAYS :always
|
||||
HttpClient$Redirect/NEVER :never
|
||||
HttpClient$Redirect/NORMAL :normal)
|
||||
(is (thrown? Exception (build-http-client {:redirect-policy :not-valid-value}))))
|
||||
|
||||
(testing "priority"
|
||||
(is (build-http-client {:priority 1}))
|
||||
(is (build-http-client {:priority 256}))
|
||||
(is (thrown? Exception (build-http-client {:priority :not-a-number})))
|
||||
(are [x] (thrown? Exception (build-http-client {:priority x}))
|
||||
:not-a-number
|
||||
0
|
||||
257))
|
||||
|
||||
(testing "proxy"
|
||||
(is (.isEmpty (.proxy (build-http-client {}))) "not set by default")
|
||||
(is (.isPresent (.proxy (build-http-client {:proxy :no-proxy}))))
|
||||
(is (.isPresent (.proxy (build-http-client {:proxy (ProxySelector/getDefault)})))))
|
||||
|
||||
#_(testing "executor"
|
||||
(let [executor (pexec/fixed-pool 1)
|
||||
client (build-http-client {:executor executor})
|
||||
stored-executor (.orElse (.executor client) nil)]
|
||||
(is (instance? java.util.concurrent.ThreadPoolExecutor stored-executor) "executor has proper type")
|
||||
(is (= executor stored-executor) "executor set properly")))
|
||||
|
||||
(testing "ssl-context"
|
||||
(is (= (SSLContext/getDefault) (.sslContext (build-http-client {}))))
|
||||
(is (not= (SSLContext/getDefault) (.sslContext (build-http-client {:ssl-context {:keystore (io/resource "keystore.p12")
|
||||
:keystore-pass "borisman"
|
||||
:trust-store (io/resource "keystore.p12")
|
||||
:trust-store-pass "borisman"}})))))
|
||||
|
||||
(testing "version"
|
||||
(is (= HttpClient$Version/HTTP_2 (.version (build-http-client {}))) "HTTP_2 by default")
|
||||
(are [expected option] (= expected (.version (build-http-client {:version option})))
|
||||
HttpClient$Version/HTTP_1_1 :http-1.1
|
||||
HttpClient$Version/HTTP_2 :http-2)
|
||||
(is (thrown? Exception (build-http-client {:version :not-valid-value})))))
|
||||
|
||||
(deftest ^:integration test-basic-response
|
||||
(testing "basic get request returns response map"
|
||||
(let [r (get "https://httpbin.org/get")]
|
||||
(is (pos-int? (:request-time r)))
|
||||
(is (= 200 (:status r)))
|
||||
(is (= "https://httpbin.org/get" (:uri r)))
|
||||
(is (= :http-2 (:version r)))
|
||||
(is (= :get (-> r :request :request-method)))
|
||||
(is (= "gzip, deflate" (get-in r [:request :headers "accept-encoding"])))))
|
||||
|
||||
#_(testing "setting executor works"
|
||||
(let [c (build-http-client {:executor (pexec/fixed-pool 1)})
|
||||
r (get "https://httpbin.org/get" {:http-client c})]
|
||||
(is (= 200 (:status r)))))
|
||||
|
||||
(testing "query encoding"
|
||||
(let [r (get "https://httpbin.org/get?foo=bar<bee")]
|
||||
(is (= "https://httpbin.org/get?foo=bar%3Cbee" (:uri r)) "encodes illegals"))
|
||||
|
||||
(let [r (get "https://httpbin.org/get?foo=bar%3Cbee")]
|
||||
(is (= "https://httpbin.org/get?foo=bar%3Cbee" (:uri r)) "does not double encode")))
|
||||
|
||||
(testing "verbs exist"
|
||||
(are [fn] (= 200 (:status (fn "https://httpbin.org/status/200")))
|
||||
get
|
||||
post
|
||||
put
|
||||
patch
|
||||
delete
|
||||
head
|
||||
options)))
|
||||
|
||||
#_(deftest ^:integration test-multipart-response
|
||||
(testing "basic post request returns response map"
|
||||
(with-server (ring.middleware.multipart-params/wrap-multipart-params
|
||||
(fn app [req]
|
||||
{:status 200
|
||||
:headers {"Content-Type" "application/json"}
|
||||
:body (let [params (clojure.walk/keywordize-keys (:multipart-params req))]
|
||||
(json/generate-string {:files {:file (-> params :file :tempfile slurp)}
|
||||
:form (select-keys params [:Content/type :eggplant :title])}))}))
|
||||
|
||||
(let [uuid (.toString (UUID/randomUUID))
|
||||
_ (spit (io/file ".test-data") uuid)
|
||||
r (post "http://localhost:1234" {:as :json
|
||||
:multipart [{:name "title" :content "My Awesome Picture"}
|
||||
{:name "Content/type" :content "image/jpeg"}
|
||||
{:name "foo.txt" :part-name "eggplant" :content "Eggplants"}
|
||||
{:name "file" :content (io/file ".test-data")}]})]
|
||||
|
||||
(is (= {:files {:file uuid}
|
||||
:form {:Content/type "image/jpeg"
|
||||
:eggplant "Eggplants"
|
||||
:title "My Awesome Picture"}} (:body r)))))))
|
||||
|
||||
(deftest ^:integration test-basic-response-async
|
||||
(testing "basic request returns something"
|
||||
(let [r @(request {:url "https://httpbin.org/get" :async? true})]
|
||||
(is (= 200 (:status r)))))
|
||||
|
||||
(testing "basic get request returns response map"
|
||||
(let [r @(get "https://httpbin.org/get" {:async? true})]
|
||||
(is (pos-int? (:request-time r)))
|
||||
(is (= 200 (:status r)))
|
||||
(is (= "https://httpbin.org/get" (:uri r)))
|
||||
(is (= :http-2 (:version r)))
|
||||
(is (= :get (-> r :request :request-method)))
|
||||
(is (= "gzip, deflate" (get-in r [:request :headers "accept-encoding"])))))
|
||||
|
||||
(testing "callback"
|
||||
(is (= 200 @(get "https://httpbin.org/status/200" {:async? true} :status identity))
|
||||
"returns status on success")
|
||||
(is (= 400 @(get "https://httpbin.org/status/400" {:async? true} identity #(-> % ex-data :status)))
|
||||
"extracts response map from ex-info on fail")))
|
||||
|
||||
(deftest ^:integration test-exceptions
|
||||
(testing "throws on exceptional status"
|
||||
(is (thrown? Exception (get "https://httpbin.org/status/500"))))
|
||||
|
||||
(testing "can opt out"
|
||||
(is (= 500 (:status (get "https://httpbin.org/status/500" {:throw-exceptions false})))))
|
||||
|
||||
(testing "async"
|
||||
(is (thrown? Exception @(get "https://httpbin.org/status/400" {:async? true}))
|
||||
"default callbacks throws on error")
|
||||
(is (= 400 (:status @(get "https://httpbin.org/status/400" {:async? true :throw-exceptions? false})))
|
||||
"default callbacks does not throw if :throw-exceptions? is false")))
|
||||
|
||||
(deftest ^:integration test-coercions
|
||||
(testing "as default"
|
||||
(let [r (get "https://httpbin.org/get")]
|
||||
(is (string? (:body r)))))
|
||||
|
||||
(testing "as byte array"
|
||||
(let [r (get "https://httpbin.org/get" {:as :byte-array})]
|
||||
(is (instance? (Class/forName "[B") (:body r)))
|
||||
(is (string? (String. ^bytes (:body r))))))
|
||||
|
||||
(testing "as stream"
|
||||
(let [r (get "https://httpbin.org/get" {:as :stream})]
|
||||
(is (instance? InputStream (:body r)))
|
||||
(is (string? (-> r :body slurp)))))
|
||||
|
||||
(testing "as string"
|
||||
(let [r (get "https://httpbin.org/get" {:as :string})]
|
||||
(is (string? (:body r)))))
|
||||
|
||||
#_(testing "as auto"
|
||||
(let [r (get "https://httpbin.org/get" {:as :auto})]
|
||||
(is (coll? (:body r)))))
|
||||
|
||||
(testing "as json"
|
||||
(let [r (get "https://httpbin.org/get" {:as :json})]
|
||||
(is (coll? (:body r)))))
|
||||
|
||||
(testing "large json"
|
||||
; Ensure large json can be parsed without the stream closing prematurely.
|
||||
(with-server (fn app [_]
|
||||
{:status 200
|
||||
:headers {"Content-Type" "application/json"}
|
||||
:body (json/generate-string (take 1000 (repeatedly (constantly {:a 1}))))})
|
||||
|
||||
(let [b (:body (get "http://localhost:1234" {:as :json}))]
|
||||
(is (coll? b))
|
||||
(is (= 1000 (count b))))))
|
||||
|
||||
(doseq [content-type [#_"msgpack" "json"]]
|
||||
(testing (str "decode " content-type)
|
||||
(let [body {:a [1 2]}]
|
||||
(with-server (fn app [_]
|
||||
{:status 200
|
||||
:headers {"Content-Type" (str "application/transit+" content-type)}
|
||||
:body (let [output (ByteArrayOutputStream.)]
|
||||
(transit/write (transit/writer output (keyword content-type)) body)
|
||||
(clojure.java.io/input-stream (.toByteArray output)))})
|
||||
|
||||
(let [r (get "http://localhost:1234" {:as :auto})]
|
||||
(is (= body (:body r)))))))
|
||||
|
||||
(testing (str "empty stream when decoding " content-type)
|
||||
(with-server (fn app [_]
|
||||
{:status 200
|
||||
:headers {"Content-Type" (str "application/transit+" content-type)}
|
||||
:body nil})
|
||||
|
||||
(let [r (get "http://localhost:1234" {:as :auto})]
|
||||
(is (nil? (:body r))))))))
|
||||
|
||||
(deftest ^:integration test-auth
|
||||
(testing "authenticator basic auth (non-preemptive) via client option"
|
||||
(let [r (get "https://httpbin.org/basic-auth/user/pass" {:http-client {:authenticator {:user "user" :pass "pass"}}})]
|
||||
(is (= 200 (:status r))))
|
||||
|
||||
(is (thrown? Exception (get "https://httpbin.org/basic-auth/user/pass" {:http-client {:authenticator {:user "user" :pass "invalid"}}}))))
|
||||
|
||||
(testing "basic auth"
|
||||
(let [r (get "https://httpbin.org/basic-auth/user/pass" {:basic-auth {:user "user" :pass "pass"}})]
|
||||
(is (= 200 (:status r))))
|
||||
|
||||
(let [r (get "https://user:pass@httpbin.org/basic-auth/user/pass")]
|
||||
(is (= 200 (:status r))))
|
||||
|
||||
(is (thrown? Exception (get "https://httpbin.org/basic-auth/user/pass" {:basic-auth {:user "user" :pass "invalid"}})))))
|
||||
|
||||
(deftest ^:integration test-redirects
|
||||
; Changed provider due to https://github.com/postmanlabs/httpbin/issues/617
|
||||
(let [redirect-to "https://httpbingo.org/get"
|
||||
uri (format "https://httpbingo.org/redirect-to?url=%s" redirect-to)]
|
||||
(testing "no redirects (default)"
|
||||
(let [r (get uri {:as :string})]
|
||||
(is (= 302 (:status r)))
|
||||
(is (= uri (:uri r)))))
|
||||
|
||||
(testing "explicitly never"
|
||||
(let [r (get uri {:as :string :http-client {:redirect-policy :never}})]
|
||||
(is (= 302 (:status r)))
|
||||
(is (= uri (:uri r)))))
|
||||
|
||||
(testing "always redirect"
|
||||
(let [r (get uri {:as :string :http-client {:redirect-policy :always}})]
|
||||
(is (= 200 (:status r)))
|
||||
(is (= redirect-to (:uri r)))))
|
||||
|
||||
(testing "normal redirect (same protocol - accepted)"
|
||||
(let [r (get uri {:as :string :http-client {:redirect-policy :normal}})]
|
||||
(is (= 200 (:status r)))
|
||||
(is (= redirect-to (:uri r)))))
|
||||
|
||||
(testing "normal redirect (different protocol - denied)"
|
||||
(let [https-tp-http-uri (format "https://httpbingo.org/redirect-to?url=%s" "http://httpbingo.org/get")
|
||||
r (get https-tp-http-uri {:as :string :http-client {:redirect-policy :normal}})]
|
||||
(is (= 302 (:status r)))
|
||||
(is (= https-tp-http-uri (:uri r)))))
|
||||
|
||||
(testing "default max redirects"
|
||||
(are [status redirects] (= status (:status (get (str "https://httpbingo.org/redirect/" redirects) {:http-client {:redirect-policy :normal}})))
|
||||
200 4
|
||||
302 5))))
|
||||
|
||||
(deftest ^:integration test-cookies
|
||||
(testing "no cookie manager"
|
||||
(let [r (get "https://httpbin.org/cookies/set/moo/cow" {:as :json :http-client {:redirect-policy :always}})]
|
||||
(is (= 200 (:status r)))
|
||||
(is (nil? (-> r :body :cookies :moo)))))
|
||||
|
||||
(testing "with cookie manager"
|
||||
(let [r (get "https://httpbin.org/cookies/set/moo/cow" {:as :json :http-client {:redirect-policy :always :cookie-policy :all}})]
|
||||
(is (= 200 (:status r)))
|
||||
(is (= "cow" (-> r :body :cookies :moo)))))
|
||||
|
||||
(testing "persists over requests"
|
||||
(let [c (build-http-client {:redirect-policy :always :cookie-policy :all})
|
||||
_ (get "https://httpbin.org/cookies/set/moo/cow" {:http-client c})
|
||||
r (get "https://httpbin.org/cookies" {:as :json :http-client c})]
|
||||
(is (= 200 (:status r)))
|
||||
(is (= "cow" (-> r :body :cookies :moo))))))
|
||||
|
||||
(deftest ^:integration test-decompression
|
||||
(testing "gzip via byte array"
|
||||
(let [r (get "https://httpbin.org/gzip" {:as :json})]
|
||||
(is (= 200 (:status r)))
|
||||
(is (true? (-> r :body :gzipped)))))
|
||||
|
||||
(testing "gzip via stream"
|
||||
(let [r (get "https://httpbin.org/gzip" {:as :stream})]
|
||||
(is (= 200 (:status r)))
|
||||
(is (instance? InputStream (:body r)))))
|
||||
|
||||
(testing "deflate via byte array"
|
||||
(let [r (get "https://httpbin.org/deflate" {:as :json})]
|
||||
(is (= 200 (:status r)))
|
||||
(is (true? (-> r :body :deflated)))))
|
||||
|
||||
(testing "deflate via stream"
|
||||
(let [r (get "https://httpbin.org/deflate" {:as :stream})]
|
||||
(is (= 200 (:status r)))
|
||||
(is (instance? InputStream (:body r))))))
|
||||
|
||||
(deftest ^:integration test-http2
|
||||
(testing "can make an http2 request"
|
||||
(let [r (get "https://nghttp2.org/httpbin/get" {:as :json})]
|
||||
(is (= :http-2 (:version r))))))
|
||||
|
||||
(deftest custom-middleware
|
||||
(let [r (get "" {:middleware [(fn [client]
|
||||
(fn [req]
|
||||
::response))]})]
|
||||
(is (= ::response r))))
|
||||
|
||||
(try (get "https://httpbin.org/gzip" {:timeout 1000})
|
||||
(catch java.net.http.HttpTimeoutException _
|
||||
(doseq [v (vals (ns-publics *ns*))]
|
||||
(when (:integration (meta v))
|
||||
(println "Removing test from" v "because httpbin is slow.")
|
||||
(alter-meta! v dissoc :test)))))
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
(ns spartan.spec-test
|
||||
(:require [clojure.test :as t :refer [deftest is]]))
|
||||
|
||||
(time (require '[spartan.spec :as s]))
|
||||
(require '[spartan.spec :as s])
|
||||
(time (require '[spartan.spec]))
|
||||
(require '[clojure.spec.alpha :as s])
|
||||
|
||||
(deftest spec-test
|
||||
(time (s/explain (s/cat :i int? :s string?) [1 :foo]))
|
||||
|
|
|
|||
|
|
@ -793,10 +793,19 @@ true")))
|
|||
(is (= "192.168.2.2" (bb nil "(-> (java.net.InetAddress/getByName \"192.168.2.2\") (.getHostAddress))"))))
|
||||
|
||||
(deftest satisfies-protocols-test
|
||||
#_(is (true? (bb nil "(satisfies? clojure.core.protocols/Datafiable {})")))
|
||||
#_(is (true? (bb nil "(satisfies? clojure.core.protocols/Navigable {})")))
|
||||
(is (true? (bb nil "(satisfies? clojure.core.protocols/Datafiable {})")))
|
||||
(is (true? (bb nil "(satisfies? clojure.core.protocols/Navigable {})")))
|
||||
(is (true? (bb nil "(satisfies? clojure.core.protocols/IKVReduce {})"))))
|
||||
|
||||
(deftest interop-on-proxy
|
||||
(is (true? (bb nil (pr-str
|
||||
'(instance? java.net.PasswordAuthentication
|
||||
(.getPasswordAuthentication
|
||||
(proxy [java.net.Authenticator] []
|
||||
(getPasswordAuthentication []
|
||||
(java.net.PasswordAuthentication. "bork"
|
||||
(char-array "dude")))))))))))
|
||||
|
||||
;;;; Scratch
|
||||
|
||||
(comment
|
||||
|
|
|
|||
Loading…
Reference in a new issue