From 83b2e90ca78e876f26b36f62be928b6ec66b5639 Mon Sep 17 00:00:00 2001 From: Daw-Ran Liou Date: Mon, 30 Sep 2019 15:48:08 -0700 Subject: [PATCH 1/6] Add reitit.ring.middleware.session ns --- .../src/reitit/ring/middleware/session.clj | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 modules/reitit-middleware/src/reitit/ring/middleware/session.clj 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..2e2e7eb79 --- /dev/null +++ b/modules/reitit-middleware/src/reitit/ring/middleware/session.clj @@ -0,0 +1,23 @@ +(ns reitit.ring.middleware.session + (:require + [ring.middleware.session :as session] + [ring.middleware.session.memory :as memory])) + +(def ^:private store + "The in-memory session store. + + This is used when no `:session` key is provided to the middleware." + (atom {})) + +(def session-middleware + "Middleware for session. + + Decodes the session from a request map into the `:session` value and updates the session store + based on the response's `:session` value. + + | key | description | + | -------------|-------------| + | `:session` | `ring.middleware.session.store/SessionStore` instance. Use `ring.middleware.session.memory/MemoryStore` by default." + {:name :session + :compile (fn [{:keys [session] :or {session {:store (memory/memory-store store)}}} _] + {:wrap #(session/wrap-session % session)})}) From 888856b5cc4ef813f5697b38696f1f9a815b8692 Mon Sep 17 00:00:00 2001 From: Daw-Ran Liou Date: Mon, 30 Sep 2019 16:30:52 -0700 Subject: [PATCH 2/6] Add tests for session middleware --- .../reitit/ring/middleware/session_test.clj | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 test/clj/reitit/ring/middleware/session_test.clj 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..ca4acfe7c --- /dev/null +++ b/test/clj/reitit/ring/middleware/session_test.clj @@ -0,0 +1,60 @@ +(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.ring :as ring])) + +(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)] + (-> 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 + (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}}})] + (is (= (count @store) + 1)) + (is (-> @store first second) + {:counter 2}))) + +(deftest default-session-test + (let [app (ring/ring-handler + (ring/router + ["/api" + {: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}}})] + (is (= (inc (get-in first-response [:body :counter])) + (get-in second-response [:body :counter]))))) From 78a1cc144ed70fafabfc94b3e82254c7c15137c5 Mon Sep 17 00:00:00 2001 From: Daw-Ran Liou Date: Tue, 1 Oct 2019 07:18:19 -0700 Subject: [PATCH 3/6] Add spec --- .../src/reitit/ring/middleware/session.clj | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/modules/reitit-middleware/src/reitit/ring/middleware/session.clj b/modules/reitit-middleware/src/reitit/ring/middleware/session.clj index 2e2e7eb79..e90963e19 100644 --- a/modules/reitit-middleware/src/reitit/ring/middleware/session.clj +++ b/modules/reitit-middleware/src/reitit/ring/middleware/session.clj @@ -1,10 +1,13 @@ (ns reitit.ring.middleware.session (:require + [clojure.spec.alpha :as s] [ring.middleware.session :as session] [ring.middleware.session.memory :as memory])) +(s/def ::spec (s/keys :opt-un [::session])) + (def ^:private store - "The in-memory session store. + "The default shared in-memory session store. This is used when no `:session` key is provided to the middleware." (atom {})) @@ -12,12 +15,19 @@ (def session-middleware "Middleware for session. - Decodes the session from a request map into the `:session` value and updates the session store - based on the response's `:session` value. + 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` | `ring.middleware.session.store/SessionStore` instance. Use `ring.middleware.session.memory/MemoryStore` by default." {:name :session - :compile (fn [{:keys [session] :or {session {:store (memory/memory-store store)}}} _] + :spec ::spec + :compile (fn [{:keys [session] + :or {session {:store (memory/memory-store store)}}} _] {:wrap #(session/wrap-session % session)})}) From f46244cc487e1e892ffa6a541be28bc6ec4bc27f Mon Sep 17 00:00:00 2001 From: Daw-Ran Liou Date: Tue, 1 Oct 2019 12:53:19 -0700 Subject: [PATCH 4/6] Fix spec and docstring Document specs for the :session entity map. Fix the default session options. Add test to validate spec. --- modules/reitit-middleware/.nrepl-port | 1 + .../src/reitit/ring/middleware/session.clj | 18 ++-- .../reitit/ring/middleware/session_test.clj | 85 ++++++++++++------- 3 files changed, 65 insertions(+), 39 deletions(-) create mode 100644 modules/reitit-middleware/.nrepl-port diff --git a/modules/reitit-middleware/.nrepl-port b/modules/reitit-middleware/.nrepl-port new file mode 100644 index 000000000..d0d8aacb2 --- /dev/null +++ b/modules/reitit-middleware/.nrepl-port @@ -0,0 +1 @@ +43803 \ No newline at end of file diff --git a/modules/reitit-middleware/src/reitit/ring/middleware/session.clj b/modules/reitit-middleware/src/reitit/ring/middleware/session.clj index e90963e19..e03002044 100644 --- a/modules/reitit-middleware/src/reitit/ring/middleware/session.clj +++ b/modules/reitit-middleware/src/reitit/ring/middleware/session.clj @@ -2,15 +2,21 @@ (: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 `:session` key is provided to the middleware." - (atom {})) + This is used when no `:store` key is provided to the middleware." + (memory/memory-store (atom {}))) (def session-middleware "Middleware for session. @@ -25,9 +31,9 @@ | key | description | | -------------|-------------| - | `:session` | `ring.middleware.session.store/SessionStore` instance. Use `ring.middleware.session.memory/MemoryStore` by default." + | `: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`." {:name :session :spec ::spec - :compile (fn [{:keys [session] - :or {session {:store (memory/memory-store store)}}} _] - {:wrap #(session/wrap-session % session)})}) + :compile (fn [{session-opts :session} _] + (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 index ca4acfe7c..efaf41f7d 100644 --- a/test/clj/reitit/ring/middleware/session_test.clj +++ b/test/clj/reitit/ring/middleware/session_test.clj @@ -2,7 +2,9 @@ (:require [clojure.test :refer [deftest testing is]] [reitit.ring.middleware.session :as session] [ring.middleware.session.memory :as memory] - [reitit.ring :as ring])) + [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." @@ -24,37 +26,54 @@ :session {:counter counter}})) (deftest session-test - (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}}})] - (is (= (count @store) - 1)) - (is (-> @store first second) - {:counter 2}))) + (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 - (let [app (ring/ring-handler - (ring/router - ["/api" - {: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}}})] - (is (= (inc (get-in first-response [:body :counter])) - (get-in second-response [:body :counter]))))) + (testing "Default session store" + (let [app (ring/ring-handler + (ring/router + ["/api" + {: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 (= (inc (get-in first-response [:body :counter])) + (get-in second-response [:body :counter]))))))) + +(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}))))))) From 99efdeea2da699269c45c412169adaef01c4cdd4 Mon Sep 17 00:00:00 2001 From: Daw-Ran Liou Date: Thu, 3 Oct 2019 07:30:39 -0700 Subject: [PATCH 5/6] Remove .nrepl-port Recursively ignore .nrepl-port in the sub-directories. --- .gitignore | 2 +- modules/reitit-middleware/.nrepl-port | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 modules/reitit-middleware/.nrepl-port 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/.nrepl-port b/modules/reitit-middleware/.nrepl-port deleted file mode 100644 index d0d8aacb2..000000000 --- a/modules/reitit-middleware/.nrepl-port +++ /dev/null @@ -1 +0,0 @@ -43803 \ No newline at end of file From a3c64baeba60f9330da001c72b579b5425b7c9bf Mon Sep 17 00:00:00 2001 From: Daw-Ran Liou Date: Fri, 4 Oct 2019 10:48:53 -0700 Subject: [PATCH 6/6] Change session middleware's default to off When `:session` key is absent in the route data, the session middleware will not be attached to the route. To enable the middleware, the user at least need to use an empty map `{}` for the `:session`, which uses the default options. --- .../src/reitit/ring/middleware/session.clj | 7 +++--- .../reitit/ring/middleware/session_test.clj | 25 ++++++++++++++----- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/modules/reitit-middleware/src/reitit/ring/middleware/session.clj b/modules/reitit-middleware/src/reitit/ring/middleware/session.clj index e03002044..ea3a0cf01 100644 --- a/modules/reitit-middleware/src/reitit/ring/middleware/session.clj +++ b/modules/reitit-middleware/src/reitit/ring/middleware/session.clj @@ -31,9 +31,10 @@ | 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`." + | `: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} _] - (let [session-opts (merge {:store store} session-opts)] - {:wrap #(session/wrap-session % session-opts)}))}) + (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 index efaf41f7d..5006d57c1 100644 --- a/test/clj/reitit/ring/middleware/session_test.clj +++ b/test/clj/reitit/ring/middleware/session_test.clj @@ -11,11 +11,11 @@ [request] (let [pattern #"ring-session=([-\w]+);Path=/;HttpOnly" parse-fn (partial re-find pattern)] - (-> request - (get-in [:headers "Set-Cookie"]) - first - parse-fn - second))) + (some-> request + (get-in [:headers "Set-Cookie"]) + first + parse-fn + second))) (defn handler "The handler that increments the counter." @@ -52,7 +52,8 @@ (let [app (ring/ring-handler (ring/router ["/api" - {:middleware [session/session-middleware]} + {:middleware [session/session-middleware] + :session {}} ["/ping" handler] ["/pong" handler]])) first-response (app {:request-method :get @@ -65,6 +66,18 @@ (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"