diff --git a/.gitignore b/.gitignore index 7b183f214..89a2290bf 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ pom.xml.asc *.jar *.class /.lein-* -/.nrepl-port +.nrepl-port /.nrepl-history /gh-pages /node_modules diff --git a/modules/reitit-middleware/src/reitit/ring/middleware/session.clj b/modules/reitit-middleware/src/reitit/ring/middleware/session.clj new file mode 100644 index 000000000..ea3a0cf01 --- /dev/null +++ b/modules/reitit-middleware/src/reitit/ring/middleware/session.clj @@ -0,0 +1,40 @@ +(ns reitit.ring.middleware.session + (:require + [clojure.spec.alpha :as s] + [ring.middleware.session :as session] + [ring.middleware.session.store :as session-store] + [ring.middleware.session.memory :as memory])) + +(s/def ::store #(satisfies? session-store/SessionStore %)) +(s/def ::root string?) +(s/def ::cookie-name string?) +(s/def ::cookie-attrs map?) +(s/def ::session (s/keys :opt-un [::store ::root ::cookie-name ::cookie-attrs])) +(s/def ::spec (s/keys :opt-un [::session])) + +(def ^:private store + "The default shared in-memory session store. + + This is used when no `:store` key is provided to the middleware." + (memory/memory-store (atom {}))) + +(def session-middleware + "Middleware for session. + + Enter: + Add the `:session` key into the request map based on the `:cookies` + in the request map. + + Exit: + When `:session` key presents in the response map, update the session + store with its value. Then remove `:session` from the response map. + + | key | description | + | -------------|-------------| + | `:session` | A map of options that passes into the [`ring.middleware.session/wrap-session](http://ring-clojure.github.io/ring/ring.middleware.session.html#var-wrap-session) function`, or an empty map for the default options. The absence of this value will disable the middleware." + {:name :session + :spec ::spec + :compile (fn [{session-opts :session} _] + (if session-opts + (let [session-opts (merge {:store store} session-opts)] + {:wrap #(session/wrap-session % session-opts)})))}) diff --git a/test/clj/reitit/ring/middleware/session_test.clj b/test/clj/reitit/ring/middleware/session_test.clj new file mode 100644 index 000000000..5006d57c1 --- /dev/null +++ b/test/clj/reitit/ring/middleware/session_test.clj @@ -0,0 +1,92 @@ +(ns reitit.ring.middleware.session-test + (:require [clojure.test :refer [deftest testing is]] + [reitit.ring.middleware.session :as session] + [ring.middleware.session.memory :as memory] + [reitit.spec :as rs] + [reitit.ring :as ring] + [reitit.ring.spec :as rrs])) + +(defn get-session-id + "Parse the session-id out of response headers." + [request] + (let [pattern #"ring-session=([-\w]+);Path=/;HttpOnly" + parse-fn (partial re-find pattern)] + (some-> request + (get-in [:headers "Set-Cookie"]) + first + parse-fn + second))) + +(defn handler + "The handler that increments the counter." + [{session :session}] + (let [counter (inc (:counter session 0))] + {:status 200 + :body {:counter counter} + :session {:counter counter}})) + +(deftest session-test + (testing "Custom session store" + (let [store (atom {}) + app (ring/ring-handler + (ring/router + ["/api" + {:session {:store (memory/memory-store store)} + :middleware [session/session-middleware]} + ["/ping" handler] + ["/pong" handler]])) + first-response (app {:request-method :get + :uri "/api/ping"}) + session-id (get-session-id first-response) + second-response (app {:request-method :get + :uri "/api/pong" + :cookies {"ring-session" {:value session-id}}})] + (testing "shared across routes" + (is (= (count @store) + 1)) + (is (-> @store first second) + {:counter 2}))))) + +(deftest default-session-test + (testing "Default session store" + (let [app (ring/ring-handler + (ring/router + ["/api" + {:middleware [session/session-middleware] + :session {}} + ["/ping" handler] + ["/pong" handler]])) + first-response (app {:request-method :get + :uri "/api/ping"}) + session-id (get-session-id first-response) + second-response (app {:request-method :get + :uri "/api/pong" + :cookies {"ring-session" {:value session-id}}})] + (testing "shared across routes" + (is (= (inc (get-in first-response [:body :counter])) + (get-in second-response [:body :counter]))))))) + +(deftest default-session-off-test + (testing "Default session middleware" + (let [app (ring/ring-handler + (ring/router + ["/api" + {:middleware [session/session-middleware]} + ["/ping" handler]])) + resp (app {:request-method :get + :uri "/api/ping"})] + (testing "off by default" + (is (nil? (get-session-id resp))))))) + +(deftest session-spec-test + (testing "Session spec" + (testing "with invalid session store type" + (is + (thrown? Exception + (ring/ring-handler + (ring/router + ["/api" + {:session {:store nil} + :middleware [session/session-middleware] + :handler handler}] + {:validate rrs/validate})))))))