diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..134c447d --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,68 @@ +version: 2.1 + +executor_defaults: &executor_defaults + working_directory: ~/repo + +# We exercise the following JVMs: +# * those officially supported by Clojure (atm: 8 and 11) +# * plus, whatever the latest version is. +executors: + openjdk8: + docker: + - image: circleci/clojure:openjdk-8-lein-2.9.5 + environment: + LEIN_ROOT: "true" + JVM_OPTS: -Xmx3200m + <<: *executor_defaults + openjdk11: + docker: + - image: circleci/clojure:openjdk-11-lein-2.9.5 + environment: + LEIN_ROOT: "true" + JVM_OPTS: -Xmx3200m --illegal-access=deny + <<: *executor_defaults + openjdk16: + docker: + - image: circleci/clojure:openjdk-16-lein-2.9.5-buster + environment: + LEIN_ROOT: "true" + JVM_OPTS: -Xmx3200m --illegal-access=deny + <<: *executor_defaults + +jobs: + test: + parameters: + executor: + type: executor + clojure-version: + type: string + executor: << parameters.executor >> + steps: + - checkout + + - restore_cache: + keys: + - v1-dependencies-{{ checksum "project.clj" }} + + - run: + name: Fetch dependencies + command: | + lein with-profile +test deps + + - save_cache: + paths: + - ~/.m2 + key: v1-dependencies-{{ checksum "project.clj" }} + + - run: + name: Run test suite + command: lein with-profile -user,-dev,+test,+<< parameters.clojure-version >> do clean, test + +workflows: + default: + jobs: + - test: + matrix: + parameters: + executor: [openjdk8, openjdk11, openjdk16] + clojure-version: ["1.7", "1.8", "1.9", "1.10"] diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 07913052..00000000 --- a/.travis.yml +++ /dev/null @@ -1 +0,0 @@ -language: clojure diff --git a/Readme.md b/Readme.md index dbe0a6d3..28ea4c8d 100644 --- a/Readme.md +++ b/Readme.md @@ -72,15 +72,6 @@ More example requests: ;; Send form params as a urlencoded body (client/post "http//site.com" {:form-params {:foo "bar"}}) -;; Multipart form uploads/posts -;; a map or vector works as the multipart object. Use a vector of -;; vectors if you need to preserve order, a map otherwise. -(client/post "http//example.org" {:multipart [["title" "My Awesome Picture"] - ["Content/type" "image/jpeg"] - ["file" (clojure.java.io/file "pic.jpg")]]}) -;; Multipart values can be one of the following: -;; String, InputStream, File, or a byte-array - ;; Basic authentication (client/get "http://site.com/protected" {:basic-auth ["user" "pass"]}) (client/get "http://site.com/protected" {:basic-auth "user:pass"}) diff --git a/project.clj b/project.clj index d1173132..d9a4b372 100644 --- a/project.clj +++ b/project.clj @@ -5,15 +5,19 @@ :url "http://www.opensource.org/licenses/mit-license.php"} :dependencies [[org.clojure/clojure "1.8.0"] [slingshot "0.12.2"]] - :profiles {:dev {:dependencies [[ring/ring-jetty-adapter "1.3.2"] - [ring/ring-devel "1.3.2"]]} - :1.4 {:dependencies [[org.clojure/clojure "1.4.0"]]} - :1.5 {:dependencies [[org.clojure/clojure "1.5.0"]]} + :profiles {:test {:dependencies [[ring/ring-jetty-adapter "1.3.2"] + [ch.qos.logback/logback-classic "1.2.3" + :exclusions [org.slf4j/slf4j-api]] + [org.slf4j/jcl-over-slf4j "1.7.26"] + [org.slf4j/jul-to-slf4j "1.7.26"] + [org.slf4j/log4j-over-slf4j "1.7.26"]] + :resource-paths ["test-resources"]} :1.7 {:dependencies [[org.clojure/clojure "1.7.0"]]} :1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]} - :1.9 {:dependencies [[org.clojure/clojure "1.9.0"]]}} - :test-selectors {:default #(not (:integration %)) - :integration :integration - :all (constantly true)} - :aliases {"all" ["with-profile" "dev,1.4:dev,1.5:dev,1.6:dev,1.7:dev,1.8:dev,1.9"]} + :1.9 {:dependencies [[org.clojure/clojure "1.9.0"]]} + :1.10 {:dependencies [[org.clojure/clojure "1.10.3"]]}} + :test-selectors {:default (constantly true) + :all (constantly true) + :unit #(not (:integration %)) + :integration :integration} :checksum-deps true) diff --git a/src/clj_http/lite/client.clj b/src/clj_http/lite/client.clj index f0aeda8d..a1e0805d 100644 --- a/src/clj_http/lite/client.clj +++ b/src/clj_http/lite/client.clj @@ -1,11 +1,11 @@ (ns clj-http.lite.client "Batteries-included HTTP client." - (:use [slingshot.slingshot :only [throw+]]) (:require [clojure.string :as str] [clojure.java.io :as io] [clj-http.lite.core :as core] [clj-http.lite.links :refer [wrap-links]] - [clj-http.lite.util :as util]) + [clj-http.lite.util :as util] + [slingshot.slingshot :refer [throw+]]) (:import (java.io InputStream File) (java.net URL UnknownHostException)) (:refer-clojure :exclude (get update))) diff --git a/src/clj_http/lite/core.clj b/src/clj_http/lite/core.clj index a002df1f..80a87398 100644 --- a/src/clj_http/lite/core.clj +++ b/src/clj_http/lite/core.clj @@ -47,8 +47,9 @@ (proxy [HostnameVerifier] [] (verify [^String hostname ^SSLSession session] true))) -(defn trust-invalid-manager [] +(defn trust-invalid-manager "This allows the ssl socket to connect with invalid/self-signed SSL certs." + [] (reify X509TrustManager (getAcceptedIssuers [this] nil) (checkClientTrusted [this certs authType]) @@ -62,20 +63,20 @@ the clj-http uses ByteArrays for the bodies." [{:keys [request-method scheme server-name server-port uri query-string headers content-type character-encoding body socket-timeout - conn-timeout multipart debug insecure? save-request? follow-redirects + conn-timeout debug insecure? save-request? follow-redirects chunk-size] :as req}] (let [http-url (str (name scheme) "://" server-name (when server-port (str ":" server-port)) uri (when query-string (str "?" query-string))) _ (when insecure? - (do (HttpsURLConnection/setDefaultSSLSocketFactory + (HttpsURLConnection/setDefaultSSLSocketFactory (.getSocketFactory (doto (SSLContext/getInstance "SSL") (.init nil (into-array TrustManager [(trust-invalid-manager)]) (new SecureRandom))))) - (HttpsURLConnection/setDefaultHostnameVerifier (my-host-verifier)))) - ^HttpURLConnection conn (.openConnection ^URL (URL. http-url))] + (HttpsURLConnection/setDefaultHostnameVerifier (my-host-verifier))) + ^HttpURLConnection conn (.openConnection (URL. http-url))] (when (and content-type character-encoding) (.setRequestProperty conn "Content-Type" (str content-type "; charset=" diff --git a/test-resources/logback.xml b/test-resources/logback.xml new file mode 100644 index 00000000..bee31b99 --- /dev/null +++ b/test-resources/logback.xml @@ -0,0 +1,19 @@ + + + + + %d{ISO8601,Europe/London} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + diff --git a/test/clj_http/test/client.clj b/test/clj_http/test/client.clj index f81de0da..f81ebcf7 100644 --- a/test/clj_http/test/client.clj +++ b/test/clj_http/test/client.clj @@ -1,29 +1,23 @@ (ns clj-http.test.client - (:use [clojure.test] - [clj-http.test.core :only [run-server]]) (:require [clj-http.lite.client :as client] - [clj-http.lite.util :as util]) + [clj-http.test.core :refer [base-req current-port with-server]] + [clj-http.lite.util :as util] + [clojure.test :refer [deftest is testing use-fixtures]]) (:import (java.net UnknownHostException) (java.util Arrays))) -(def base-req - {:scheme :http - :server-name "localhost" - :server-port 18080}) +(use-fixtures :each with-server) (deftest ^{:integration true} roundtrip - (run-server) - (Thread/sleep 1000) ;; roundtrip with scheme as a keyword - (let [resp (client/request (merge base-req {:uri "/get" :method :get}))] + (let [resp (client/request (merge (base-req) {:uri "/get" :method :get}))] (is (= 200 (:status resp))) - #_(is (= "close" (get-in resp [:headers "connection"]))) (is (= "get" (:body resp)))) ;; roundtrip with scheme as a string - (let [resp (client/request (merge base-req {:uri "/get" :method :get - :scheme "http"}))] + (let [resp (client/request (merge (base-req) {:uri "/get" + :method :get + :scheme "http"}))] (is (= 200 (:status resp))) - #_(is (= "close" (get-in resp [:headers "connection"]))) (is (= "get" (:body resp))))) (defn is-passed [middleware req] diff --git a/test/clj_http/test/core.clj b/test/clj_http/test/core.clj index 66bb778f..3c81b9e8 100644 --- a/test/clj_http/test/core.clj +++ b/test/clj_http/test/core.clj @@ -1,15 +1,16 @@ (ns clj-http.test.core - (:use [clojure.test] - [clojure.java.io :only [file]]) - (:require [clojure.pprint :as pp] - [clj-http.lite.core :as core] + (:require [clj-http.lite.core :as core] [clj-http.lite.util :as util] + [clojure.pprint :as pp] + [clojure.java.io :refer [file]] + [clojure.test :refer [deftest is use-fixtures]] [ring.adapter.jetty :as ring]) - (:import (java.io ByteArrayInputStream))) + (:import (java.io ByteArrayInputStream) + (org.eclipse.jetty.server Server) + (org.eclipse.jetty.server.nio SelectChannelConnector) + (org.eclipse.jetty.server.ssl SslSelectChannelConnector))) (defn handler [req] - ;;(pp/pprint req) - ;;(println) (println) (condp = [(:request-method req) (:uri req)] [:get "/get"] {:status 200 :body "get"} @@ -29,90 +30,106 @@ (Thread/sleep 10) {:status 200 :body "timeout"}) [:delete "/delete-with-body"] - {:status 200 :body "delete-with-body"} - [:post "/multipart"] - {:status 200 :body (:body req)})) + {:status 200 :body "delete-with-body"})) + +(defn make-server ^Server [] + (ring/run-jetty handler {:port 0 ;; Use a free port + :join? false + :ssl-port 0 ;; Use a free port + :ssl? true + :keystore "test-resources/keystore" + :key-password "keykey"})) + +(def ^:dynamic *server* nil) + +(defn current-port [] + (let [^Server s *server*] + (->> s + .getConnectors + (filter (comp #{SelectChannelConnector} class)) + ^SelectChannelConnector (first) + .getLocalPort))) + +(defn current-https-port [] + (let [^Server s *server*] + (->> s + .getConnectors + (filter (comp #{SslSelectChannelConnector} class)) + ^SslSelectChannelConnector (first) + .getLocalPort))) + +(defn with-server [t] + (let [s (make-server)] + (try + (binding [*server* s] + (t)) + (finally + (-> s .stop))))) -(defn run-server - [] - (defonce server - (do - (future - (ring/run-jetty handler {:port 18080})) - (Thread/sleep 1000)))) +(use-fixtures :each with-server) -(def base-req - {:scheme :http - :server-name "localhost" - :server-port 18080}) +(defn base-req [] + {:scheme :http + :server-name (str "localhost:" (current-port)) + :port (current-port)}) (defn request [req] - (core/request (merge base-req req))) + (core/request (merge (base-req) req))) (defn slurp-body [req] (slurp (:body req))) (deftest ^{:integration true} makes-get-request - (run-server) (let [resp (request {:request-method :get :uri "/get"})] (is (= 200 (:status resp))) (is (= "get" (slurp-body resp))))) (deftest ^{:integration true} makes-head-request - (run-server) (let [resp (request {:request-method :head :uri "/head"})] (is (= 200 (:status resp))) (is (nil? (:body resp))))) (deftest ^{:integration true} sets-content-type-with-charset - (run-server) - (let [resp (request {:request-method :get :uri "/content-type" - :content-type "text/plain" :character-encoding "UTF-8"})] + (let [resp (request {:request-method :get :uri "/content-type" + :content-type "text/plain" :character-encoding "UTF-8"})] (is (= "text/plain; charset=UTF-8" (slurp-body resp))))) (deftest ^{:integration true} sets-content-type-without-charset - (run-server) (let [resp (request {:request-method :get :uri "/content-type" - :content-type "text/plain"})] + :content-type "text/plain"})] (is (= "text/plain" (slurp-body resp))))) (deftest ^{:integration true} sets-arbitrary-headers - (run-server) (let [resp (request {:request-method :get :uri "/header" - :headers {"X-My-Header" "header-val"}})] + :headers {"X-My-Header" "header-val"}})] (is (= "header-val" (slurp-body resp))))) (deftest ^{:integration true} sends-and-returns-byte-array-body - (run-server) (let [resp (request {:request-method :post :uri "/post" - :body (util/utf8-bytes "contents")})] + :body (util/utf8-bytes "contents")})] (is (= 200 (:status resp))) (is (= "contents" (slurp-body resp))))) (deftest ^{:integration true} returns-arbitrary-headers - (run-server) (let [resp (request {:request-method :get :uri "/get"})] (is (string? (get-in resp [:headers "date"]))))) (deftest ^{:integration true} returns-status-on-exceptional-responses - (run-server) (let [resp (request {:request-method :get :uri "/error"})] (is (= 500 (:status resp))))) (deftest ^{:integration true} returns-status-on-redirect - (run-server) (let [resp (request {:request-method :get :uri "/redirect" :follow-redirects false})] (is (= 302 (:status resp))))) (deftest ^{:integration true} auto-follows-on-redirect - (run-server) (let [resp (request {:request-method :get :uri "/redirect"})] (is (= 200 (:status resp))) (is (= "get" (slurp-body resp))))) (deftest ^{:integration true} sets-conn-timeout - ; indirect way of testing if a connection timeout will fail by passing in an - ; invalid argument + ;; indirect way of testing if a connection timeout will fail by passing in an + ;; invalid argument (try (request {:request-method :get :uri "/timeout" :conn-timeout -1}) (throw (Exception. "Shouldn't get here.")) @@ -120,7 +137,6 @@ (is (= IllegalArgumentException (class e)))))) (deftest ^{:integration true} sets-socket-timeout - (run-server) (try (request {:request-method :get :uri "/timeout" :socket-timeout 1}) (throw (Exception. "Shouldn't get here.")) @@ -136,52 +152,31 @@ ;; (is (= 200 (:status resp))))) (deftest ^{:integration true} self-signed-ssl-get - (let [t (doto (Thread. #(ring/run-jetty handler - {:port 8081 :ssl-port 18082 :ssl? true - :keystore "test-resources/keystore" - :key-password "keykey"})) .start)] - (Thread/sleep 1000) - (try - (is (thrown? javax.net.ssl.SSLException - (request {:request-method :get :uri "/get" - :server-port 18082 :scheme :https}))) - #_(let [resp (request {:request-method :get :uri "/get" :server-port 18082 - :scheme :https :insecure? true})] - (is (= 200 (:status resp))) - (is (= "get" (slurp-body resp)))) - (finally - (.stop t))))) - -;; (deftest ^{:integration true} multipart-form-uploads -;; (run-server) -;; (let [bytes (util/utf8-bytes "byte-test") -;; stream (ByteArrayInputStream. bytes) -;; resp (request {:request-method :post :uri "/multipart" -;; :multipart [["a" "testFINDMEtest"] -;; ["b" bytes] -;; ["c" stream] -;; ["d" (file "test-resources/keystore")]]}) -;; resp-body (apply str (map #(try (char %) (catch Exception _ "")) -;; (:body resp)))] -;; (is (= 200 (:status resp))) -;; (is (re-find #"testFINDMEtest" resp-body)) -;; (is (re-find #"byte-test" resp-body)) -;; (is (re-find #"name=\"c\"" resp-body)) -;; (is (re-find #"name=\"d\"" resp-body)))) + (let [client-opts {:request-method :get + :uri "/get" + :scheme :https + :server-name (str "localhost:" (current-https-port)) + :port (current-https-port)}] + (is (thrown? javax.net.ssl.SSLException + (request client-opts))) + (let [resp (request (assoc client-opts :insecure? true))] + (is (= 200 (:status resp))) + (is (= "get" (slurp-body resp)))))) (deftest ^{:integration true} t-save-request-obj - (run-server) (let [resp (request {:request-method :post :uri "/post" - :body (.getBytes "foo bar") - :save-request? true})] + :body (.getBytes "foo bar") + :save-request? true})] (is (= 200 (:status resp))) - (is (= {:scheme :http - :http-url "http://localhost:18080/post" + (is (= {:scheme :http + :http-url (str "http://localhost:" (current-port) "/post") :request-method :post - :uri "/post" - :server-name "localhost" - :server-port 18080} - (dissoc (:request resp) :body))))) + :uri "/post" + :server-name (str "localhost:" (current-port)) + :port (current-port)} + (-> resp + :request + (dissoc :body)))))) ;; (deftest parse-headers ;; (are [headers expected] @@ -211,7 +206,6 @@ ;; "server" "some-server"})) (deftest ^{:integration true} t-streaming-response - (run-server) (let [stream (:body (request {:request-method :get :uri "/get" :as :stream})) body (slurp stream)] (is (= "get" body)))) diff --git a/test/clj_http/test/links_test.clj b/test/clj_http/test/links_test.clj index 1813e9ee..f9128cf8 100644 --- a/test/clj_http/test/links_test.clj +++ b/test/clj_http/test/links_test.clj @@ -1,7 +1,7 @@ (ns clj-http.test.links-test "Imported from https://github.com/dakrone/clj-http/blob/217393258e7863514debece4eb7b23a7a3fa8bd9/test/clj_http/test/links_test.clj" - (:require [clj-http.lite.links :refer :all] - [clojure.test :refer :all])) + (:require [clj-http.lite.links :refer [wrap-links]] + [clojure.test :refer [deftest is testing]])) (defn- link-handler [link-header] (wrap-links (constantly {:headers {"link" link-header}}))) diff --git a/test/setup.clj b/test/setup.clj new file mode 100644 index 00000000..976a6f79 --- /dev/null +++ b/test/setup.clj @@ -0,0 +1,23 @@ +(ns setup + "This namespace will be automaticaly loaded by the test runner" + (:require + [clojure.string :as string]) + (:import + (org.eclipse.jetty.util MultiException))) + +(-> (reify Thread$UncaughtExceptionHandler + (uncaughtException [_ thread e] + ;; Omit exceptions coming from "Address already in use" because they're meaningless + ;; (these happen when one picks port 0, and after one such exception a new port will be retried successfully) + (let [omit? (or (-> ^Throwable e .getMessage #{"Address already in use"}) + (and (instance? MultiException e) + (->> ^MultiException e + .getThrowables + (every? (fn [^Throwable t] + (-> t .getMessage (.contains "Address already in use")))))))] + (when-not omit? + (-> ^Throwable e .printStackTrace) + (when (System/getenv "CI") + (System/exit 1)))))) + + (Thread/setDefaultUncaughtExceptionHandler))