diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn new file mode 100644 index 0000000..dc32721 --- /dev/null +++ b/.clj-kondo/config.edn @@ -0,0 +1,18 @@ +{:skip-comments true + :linters {:private-call {:level :warn} + :redundant-do {:level :error} + :single-key-in {:level :info} + :single-operand-comparison {:level :info} + :shadowed-var {:level :info} + :syntax {:level :error} + :unused-binding {:exclude-destructured-keys-in-fn-args true} + :unresolved-symbol {:report-duplicates true + :exclude [(clojure.test/are [thrown-on-path?]) + (cljs.test/are [thrown-on-path?]) + (clojure.test/is [thrown-on-path?]) + (cljs.test/is [thrown-on-path?])]} + :use {:level :error}} + :lint-as {clojure.test.check.properties/for-all clojure.core/let + clojure.test.check.clojure-test/defspec clojure.test/deftest} + :output {:pattern "[{{LEVEL}}] {{filename}} [{{row}}:{{coll}}]: {{message}}"} + :hooks {}} \ No newline at end of file diff --git a/.cljfmt.edn b/.cljfmt.edn new file mode 100644 index 0000000..bcd652d --- /dev/null +++ b/.cljfmt.edn @@ -0,0 +1,7 @@ +{:remove-surrounding-whitespace? true + :remove-trailing-whitespace? true + :remove-consecutive-blank-lines? false + :insert-missing-whitespace? true + :align-associative? true + :indents {clojure.spec.alpha/def [[:block 1]] + clojure.test.check.properties/for-all [[:block 1]]}} \ No newline at end of file diff --git a/.gitignore b/.gitignore index c53038e..436d824 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ pom.xml.asc /.nrepl-port .hgignore .hg/ +.lsp/ +.calva/ +.clj-kondo/.cache \ No newline at end of file diff --git a/project.clj b/project.clj index af19973..38fc40e 100644 --- a/project.clj +++ b/project.clj @@ -1,12 +1,12 @@ -(defproject duct/server.http.aleph "0.1.2" +(defproject duct/server.http.aleph "0.2.0" :description "Integrant methods for running an Aleph web server" :url "https://github.com/duct-framework/server.http.aleph" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} - :dependencies [[org.clojure/clojure "1.9.0-beta3"] - [duct/core "0.6.1"] - [aleph "0.4.3"] - [duct/logger "0.2.1"] - [integrant "0.6.1"]] + :dependencies [[org.clojure/clojure "1.10.3"] + [duct/core "0.8.0"] + [aleph "0.4.7-alpha7"] + [duct/logger "0.3.0"] + [integrant "0.8.0"]] :profiles {:dev {:dependencies [[clj-http "3.7.0"]]}}) diff --git a/src/duct/server/http/aleph.clj b/src/duct/server/http/aleph.clj index c8cc821..a597437 100644 --- a/src/duct/server/http/aleph.clj +++ b/src/duct/server/http/aleph.clj @@ -1,8 +1,12 @@ (ns duct.server.http.aleph (:require [aleph.http :as aleph] [duct.logger :as logger] + [duct.server.http.spec :as spec] [integrant.core :as ig])) +(defmethod ig/pre-init-spec :duct.server.http/aleph [_] + ::spec/config) + (defmethod ig/init-key :duct.server.http/aleph [_ {:keys [handler logger] :as opts}] (let [handler (atom (delay (:handler opts))) logger (atom logger) @@ -19,10 +23,10 @@ (defmethod ig/suspend-key! :duct.server.http/aleph [_ {:keys [handler]}] (reset! handler (promise))) -(defmethod ig/resume-key :duct.server.http/aleph [key opts old-opts old-impl] +(defmethod ig/resume-key :duct.server.http/aleph [kw opts old-opts old-impl] (if (= (dissoc opts :handler :logger) (dissoc old-opts :handler :logger)) (do (deliver @(:handler old-impl) (:handler opts)) (reset! (:logger old-impl) (:logger opts)) old-impl) - (do (ig/halt-key! key old-impl) - (ig/init-key key opts)))) + (do (ig/halt-key! kw old-impl) + (ig/init-key kw opts)))) diff --git a/src/duct/server/http/spec.clj b/src/duct/server/http/spec.clj new file mode 100644 index 0000000..ba987be --- /dev/null +++ b/src/duct/server/http/spec.clj @@ -0,0 +1,67 @@ +(ns duct.server.http.spec + (:require [clojure.spec.alpha :as s] + [duct.logger :as logger]) + (:import [duct.logger Logger] + [java.net SocketAddress] + [java.util.concurrent Executor] + [io.netty.handler.ssl SslContext])) + +(def ^:private port-max-value (int (Character/MAX_VALUE))) + +(def ^:private fn-instance? (partial partial instance?)) + +(s/def ::port + (s/or :any zero? + :specific #(< 0 % port-max-value))) + +(s/def ::socket-address + (fn-instance? SocketAddress)) + +(s/def ::bootstrap-transform ifn?) + +(s/def ::ssl-context + (fn-instance? SslContext)) + +(s/def ::pipeline-transform ifn?) + +(s/def ::executor + (s/or :none (partial = :none) + :some (fn-instance? Executor))) + +(s/def ::shutdown-executor? boolean?) + +(s/def ::request-buffer-size pos-int?) + +(s/def ::raw-stream? boolean?) + +(s/def ::rejected-handler ifn?) + +(s/def ::max-initial-line-length pos-int?) + +(s/def ::max-header-size pos-int?) + +(s/def ::max-chunk-size pos-int?) + +(s/def ::epoll? boolean?) + +(s/def ::compression? boolean?) + +(s/def ::compression-level + (s/and pos-int? + #(<= 1 % 9))) + +(s/def ::idle-timeout (complement neg-int?)) + +(s/def ::logger (fn-instance? Logger)) + +(s/def ::handler ifn?) + +(s/def ::config + (s/keys :req-un [::logger ::handler] + :opt-un [::port ::socket-address ::bootstrap-transform + ::ssl-context ::pipeline-transform ::executor + ::shutdown-executor? ::request-buffer-size + ::raw-stream? ::rejected-handler + ::max-initial-line-length ::max-header-size + ::max-chunk-size ::epoll? ::compression? + ::compression-level ::idle-timeout])) diff --git a/test/duct/server/http/aleph_test.clj b/test/duct/server/http/aleph_test.clj index 25be11e..2d6a232 100644 --- a/test/duct/server/http/aleph_test.clj +++ b/test/duct/server/http/aleph_test.clj @@ -1,19 +1,134 @@ (ns duct.server.http.aleph-test - (:import java.net.ConnectException) - (:require [clj-http.client :as http] - [clojure.test :refer :all] + (:require [aleph + [flow :as flow] + [netty :as netty]] + [clj-http.client :as http] + [clojure.spec.alpha :as s] + [clojure.test :refer [deftest is testing assert-expr do-report]] [duct.core :as duct] [duct.logger :as logger] [duct.server.http.aleph :as aleph] - [integrant.core :as ig])) + [integrant.core :as ig]) + (:import [clojure.lang ExceptionInfo] + java.net.ConnectException + [java.net InetSocketAddress] + [sun.security.provider.certpath SunCertPathBuilderException])) (defrecord TestLogger [logs] logger/Logger - (-log [_ level ns-str file line id event data] + (-log [_ _ _ _ _ _ event data] (swap! logs conj [event data]))) (duct/load-hierarchy) +(defmethod assert-expr 'thrown-on-path? [msg form] + (let [selector (nth form 1) + body (nthnext form 2)] + `(try + ~@body + (do-report {:type :fail + :message ~msg + :expected '~form + :actual "Nothing was thrown"}) + (catch ExceptionInfo e# + (let [path# (some-> (ex-data e#) + :explain + ::s/problems + first + :path)] + (if (= path# ~selector) + (do-report {:type :pass + :message ~msg + :expected '~form + :actual path#}) + (do-report {:type :fail + :message ~msg + :expected '~form + :actual path#})))) + (catch Exception e# + (do-report {:type :fail + :message ~msg + :expected '~form + :actual e#}))))) + +(deftest pre-init-spec-test + (let [logger (->TestLogger (atom [])) + response {:status 200 + :body "test"} + config {:duct.server.http/aleph {:port 3400 + :executor (flow/fixed-thread-executor 2) + :shutdown-executor? true + :request-buffer-size 8196 + :raw-stream? false + :max-initial-line-length 8196 + :max-header-size 8196 + :max-chunk-size 8196 + :epoll? false + :compression? false + :idle-timeout 0 + :logger logger + :handler (constantly response)}}] + (testing "server starts" + (let [system (ig/init config)] + (try + (let [{:keys [status body]} (http/get "http://127.0.0.1:3400/")] + (is (= 200 status)) + (is (= "test" body))) + (finally + (ig/halt! system))))) + (testing "ssl context" + (testing "servers starts" + (let [system (ig/init (assoc-in config [:duct.server.http/aleph :ssl-context] + (netty/self-signed-ssl-context)))] + (try + (is (thrown? SunCertPathBuilderException + (http/get "https://127.0.0.1:3400"))) + (finally + (ig/halt! system))))) + (testing "not valid cert" + (is (thrown-on-path? + [:ssl-context] + (-> config + (assoc-in [:duct.server.http/aleph :ssl-context] + "Not a ssl cert.") + (ig/init)))))) + (testing "socket-address" + (let [config (update config :duct.server.http/aleph + dissoc :port)] + (testing "server starts" + (let [system (-> config + (assoc-in [:duct.server.http/aleph :socket-address] + (InetSocketAddress. 3400)) + (ig/init))] + (try + (let [{:keys [status body]} (http/get "http://127.0.0.1:3400/")] + (is (= 200 status)) + (is (= "test" body))) + (finally + (ig/halt! system))))) + (testing "not valid socket" + (is (thrown-on-path? [:socket-address] + (ig/init + (assoc-in config + [:duct.server.http/aleph :socket-address] + "Invalid socket object"))))))) + (testing "executor" + (testing "server starts on :none" + (let [system (ig/init (assoc-in config + [:duct.server.http/aleph :executor] + :none))] + (try + (let [{:keys [status body]} (http/get "http://127.0.0.1:3400/")] + (is (= 200 status)) + (is (= "test" body))) + (finally + (ig/halt! system))))) + (testing "should fail on a non valid executor" + (is (thrown-on-path? [:executor :none] + (ig/init (assoc-in config + [:duct.server.http/aleph :executor] + :nothing)))))))) + (deftest key-test (is (isa? :duct.server.http/aleph :duct.server/http))) @@ -49,8 +164,9 @@ (deftest resume-and-suspend-test (let [response1 {:status 200 :headers {} :body "foo"} response2 {:status 200 :headers {} :body "bar"} - config1 {:duct.server.http/aleph {:port 3400, :handler (constantly response1)}} - config2 {:duct.server.http/aleph {:port 3400, :handler (constantly response2)}}] + logger (->TestLogger (atom [])) + config1 {:duct.server.http/aleph {:port 3400, :handler (constantly response1), :logger logger}} + config2 {:duct.server.http/aleph {:port 3400, :handler (constantly response2), :logger logger}}] (testing "suspend and resume" (let [system1 (doto (ig/init config1) ig/suspend!)