Skip to content

Commit e809e87

Browse files
committed
Integrant exercise finalized + README: test chapter
1 parent c503b68 commit e809e87

File tree

19 files changed

+962
-4
lines changed

19 files changed

+962
-4
lines changed

webstore-demo/integrant-simple-server/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- [Application State Management](#application-state-management)
88
- [Manual State Management](#manual-state-management)
99
- [State Management Using Integrant](#state-management-using-integrant)
10+
- [State Management in Tests](#state-management-in-tests)
1011
- [Comparison](#comparison)
1112
- [Personal Experiences](#personal-experiences)
1213
- [The Rest of the Application](#the-rest-of-the-application)
@@ -67,6 +68,21 @@ In REPL driven development you can then use three ```integrant.repl``` namespace
6768
- halt: halt the system (in our case stop the web server)
6869
- reset: reset the system
6970

71+
## State Management in Tests
72+
73+
One more comparison. Let's first show how to handle state in the tests in which we want to start/stop webserver. First in manual state management:
74+
75+
![alt text](doc/manual_test.png)
76+
77+
So, we just use the ```start-web-server``` and ```stop-web-server``` functions.
78+
79+
Then the same using Integrant:
80+
81+
![alt text](doc/integrant_test.png)
82+
83+
This time we can use Integrant state management. First call the Integrant ```init``` function with the system configuration and store the returned system map so that after the test we can halt the system using Integrant ```halt!``` function (stop web server in our case).
84+
85+
7086
## Comparison
7187

7288
So, which one is better? I'm not sure. For smaller applications it might be feasible just to implement your custom state management - and one less dependency in your ```deps.edn``` file. But if you have a bigger application with a lot of states and dependencies between different parts of the states then you might benefit using a dedicated state management library. I actually ran a very informal gallup in Metosin Slack regarding whether Metosin programmers favor custom state management or use some state management library. Two developers favored custom state management, others some 8 guys (which clicked either emoji in Slack) favored state management, all of them Integrant. Considering that these guys are pretty seasoned Clojure programmers I would say that you won't miss your target if you choose Integrant.

webstore-demo/integrant-simple-server/deps.edn

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33

44
:aliases {
55
:dev {:extra-paths ["dev-resources"]
6-
:extra-deps {org.clojure/clojure {:mvn/version "1.10.1"}}}
6+
:extra-deps {org.clojure/clojure {:mvn/version "1.10.1"}
7+
}}
78

89
:test {:extra-paths ["test/clj"]}
910

@@ -12,7 +13,8 @@
1213
;; Let's learn to use a common part already now since in the next exercise we create frontend with re-frame.
1314
:common {:extra-paths ["src/cljc"]
1415
:extra-deps {metosin/reitit {:mvn/version "0.4.2"}
15-
integrant/repl {:mvn/version "0.3.1"}}}
16+
integrant/repl {:mvn/version "0.3.1"}
17+
}}
1618

1719
:backend {:extra-paths ["src/clj"]
1820
:extra-deps {
28.7 KB
Loading
29.2 KB
Loading
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
(ns simpleserver.session.session-common
2+
(:require [clojure.tools.logging :as log]
3+
[buddy.sign.jwt :as buddy-jwt]
4+
[clj-time.core :as c-time]
5+
[simpleserver.util.config :as ss-config]
6+
))
7+
8+
(def my-hex-secret
9+
"Creates dynamically a hex secret when the server boots."
10+
((fn []
11+
(let [my-chars (->> (range (int \a) (inc (int \z))) (map char))
12+
my-ints (->> (range (int \0) (inc (int \9))) (map char))
13+
my-set (lazy-cat my-chars my-ints)
14+
hexify (fn [s]
15+
(apply str
16+
(map #(format "%02x" (int %)) s)))]
17+
(hexify (repeatedly 24 #(rand-nth my-set)))))))
18+
19+
(defn create-json-web-token
20+
[email]
21+
(log/debug (str "ENTER create-json-web-token, email: " email))
22+
(let [my-secret my-hex-secret
23+
exp-time (c-time/plus (c-time/now) (c-time/seconds (get-in ss-config/config [:jwt :exp])))
24+
my-claim {:email email :exp exp-time}
25+
json-web-token (buddy-jwt/sign my-claim my-secret)]
26+
json-web-token))
27+
28+
(defn validate-token
29+
[token get-token remove-token]
30+
(log/debug (str "ENTER validate-token, token: " token))
31+
(let [found-token (get-token token)]
32+
;; Part #1 of validation.
33+
(if (nil? found-token)
34+
(do
35+
(log/warn (str "Token not found in my sessions - unknown token: " token))
36+
nil)
37+
;; Part #2 of validation.
38+
(try
39+
(buddy-jwt/unsign token my-hex-secret)
40+
(catch Exception e
41+
(if (.contains (.getMessage e) "Token is expired")
42+
(do
43+
(log/debug (str "Token is expired, removing it from my sessions and returning nil: " token))
44+
(remove-token token)
45+
nil)
46+
; Some other issue, throw it.
47+
(do
48+
(log/error (str "Some unknown exception when handling expired token, exception: " (.getMessage e)) ", token: " token)
49+
nil)))))))
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
(ns simpleserver.session.session-config
2+
(:require [clojure.tools.logging :as log]
3+
[simpleserver.util.config :as ss-config]
4+
[simpleserver.session.session-single-node :as ss-single-node]
5+
[simpleserver.session.session-dynamodb :as ss-dynamodb]))
6+
7+
(defn -get-session-env
8+
"Gets session environment (either single-node or aws based on config.edn)."
9+
[db-env]
10+
; See comment in domain-config which applies also here.
11+
(log/debug "ENTER -get-session-env")
12+
(cond
13+
(= db-env "single-node") (ss-single-node/->SingleNodeR)
14+
(= db-env "aws") (ss-dynamodb/->AwsDynamoDbR)
15+
:else (throw (UnsupportedOperationException. (str "Unknown environment: " db-env)))
16+
)
17+
)
18+
19+
(def session (-get-session-env (ss-config/config :db-env)))
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
(ns simpleserver.session.session-dynamodb
2+
(:require [simpleserver.session.session-interface :as ss-session-i]
3+
[simpleserver.session.session-common :as ss-session-common]
4+
[clojure.tools.logging :as log]
5+
[cognitect.aws.client.api :as aws]
6+
[simpleserver.util.config :as ss-config]))
7+
8+
(defn get-all-sessions-from-dynamodb
9+
[]
10+
(let [{my-ddb :my-ddb
11+
my-table :my-table} (ss-config/get-dynamodb-config "session")
12+
items (aws/invoke my-ddb {:op :Scan
13+
:request {
14+
:TableName my-table}})]
15+
(reduce (fn [sessions session]
16+
(conj sessions (get-in session [:token :S])))
17+
#{}
18+
(items :Items))))
19+
20+
(defn get-token
21+
[token]
22+
(let [{my-ddb :my-ddb
23+
my-table :my-table} (ss-config/get-dynamodb-config "session")
24+
result (aws/invoke my-ddb {:op :GetItem
25+
:request {:TableName my-table
26+
:Key {"token" {:S token}}}})]
27+
(get-in result [:Item :token :S])))
28+
29+
(defn remove-token
30+
[token]
31+
(let [{my-ddb :my-ddb
32+
my-table :my-table} (ss-config/get-dynamodb-config "session")
33+
result (aws/invoke my-ddb {:op :DeleteItem
34+
:request {
35+
:TableName my-table
36+
:Key {"token" {:S token}}}})]
37+
result))
38+
39+
(defrecord AwsDynamoDbR []
40+
ss-session-i/SessionInterface
41+
42+
(create-json-web-token
43+
[this email]
44+
(log/debug (str "ENTER create-json-web-token, email: " email))
45+
(let [json-web-token (ss-session-common/create-json-web-token email)
46+
{my-ddb :my-ddb
47+
my-table :my-table} (ss-config/get-dynamodb-config "session")
48+
_ (aws/invoke my-ddb {:op :PutItem
49+
:request {
50+
:TableName my-table
51+
:Item {"token" {:S json-web-token}}}})]
52+
; https://docs.aws.amazon.com/cli/latest/reference/dynamodb/put-item.html
53+
; The ReturnValues parameter is used by several DynamoDB operations; however, PutItem does not recognize any values other than NONE or ALL_OLD .
54+
json-web-token))
55+
56+
(validate-token
57+
[this token]
58+
(log/debug (str "ENTER validate-token, token: " token))
59+
(ss-session-common/validate-token token get-token remove-token)
60+
)
61+
62+
(-get-sessions
63+
[this]
64+
(log/debug (str "ENTER -get-sessions"))
65+
(get-all-sessions-from-dynamodb))
66+
67+
(-reset-sessions!
68+
[this]
69+
(log/debug (str "ENTER -reset-sessions!"))
70+
(let [sessions (get-all-sessions-from-dynamodb)
71+
]
72+
(dorun (map remove-token sessions)))))
73+
74+
(comment
75+
; Refresh interface after changes in implementation.
76+
(mydev/refresh)
77+
78+
(get-all-sessions-from-dynamodb)
79+
80+
(def my-token (let [my-aws-session simpleserver.session.session-config/session
81+
result (ss-session-i/create-json-web-token my-aws-session "kari.karttinen@foo.com")]
82+
result)
83+
)
84+
my-token
85+
(def validate-result (let [my-aws-session simpleserver.session.session-config/session
86+
result (ss-session-i/validate-token my-aws-session my-token)]
87+
result)
88+
)
89+
90+
(def sessions (let [my-aws-session simpleserver.session.session-config/session
91+
result (ss-session-i/-get-sessions my-aws-session)]
92+
result)
93+
)
94+
sessions
95+
(def reset-result (let [my-aws-session simpleserver.session.session-config/session
96+
result (ss-session-i/-reset-sessions! my-aws-session)]
97+
result)
98+
)
99+
(get-all-sessions-from-dynamodb)
100+
)
101+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
(ns simpleserver.session.session-interface)
2+
3+
(defprotocol SessionInterface
4+
(create-json-web-token [env email]
5+
"Creates the JSON web token and adds it to the sessions database.")
6+
(validate-token [env token]
7+
"Validates the token. Returns {:email :exp} from token if session ok,
8+
nil otherwise. Token validation has two parts:
9+
1. Check that we actually created the token in the first place (should find it in the session db).
10+
2. Validate the token with buddy (can unsign it, token is not expired).")
11+
(-get-sessions [env]
12+
"Gets all sessions - used in testing.")
13+
(-reset-sessions! [env]
14+
"Resets all sessions - used in testing.")
15+
)
16+
17+
18+
19+
20+
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
(ns simpleserver.session.session-single-node
2+
(:require
3+
[clojure.tools.logging :as log]
4+
[simpleserver.util.config :as ss-config]
5+
[simpleserver.session.session-interface]
6+
[simpleserver.session.session-common]))
7+
8+
(def my-sessions
9+
"Atom to store the sessions. NOTE: Not a map but a set."
10+
(atom #{}))
11+
12+
(defn get-token
13+
[token]
14+
(if (contains? @my-sessions token)
15+
token
16+
nil))
17+
18+
(defn remove-token
19+
[token]
20+
(if (contains? @my-sessions token)
21+
(swap! my-sessions disj token)
22+
(log/warn (str "Expired token not found when removing it from my sessions: " token))))
23+
24+
(defrecord SingleNodeR []
25+
simpleserver.session.session-interface/SessionInterface
26+
27+
(create-json-web-token
28+
[this email]
29+
(log/debug (str "ENTER create-json-web-token, email: " email))
30+
(let [json-web-token (simpleserver.session.session-common/create-json-web-token email)
31+
_ (swap! my-sessions conj json-web-token)]
32+
json-web-token))
33+
34+
(validate-token
35+
[this token]
36+
(log/debug (str "ENTER validate-token, token: " token))
37+
(simpleserver.session.session-common/validate-token token get-token remove-token))
38+
39+
(-get-sessions
40+
[this]
41+
(log/debug (str "ENTER -get-sessions"))
42+
@my-sessions)
43+
44+
(-reset-sessions!
45+
[this]
46+
(log/debug (str "ENTER -reset-sessions!"))
47+
(if (= (ss-config/config :runtime-env) "dev")
48+
(reset! my-sessions #{})
49+
(throw (java.lang.UnsupportedOperationException. "You can reset sessions only in development environment!"))))
50+
)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
(ns simpleserver.user.user-common
2+
(:require [simpleserver.util.config :as ss-config]
3+
[clojure.data.csv :as csv]
4+
[clojure.java.io :as io]))
5+
6+
;; NOTE: We don't use incremental user ids since it is a bit anti-pattern in DynamoDB (since email is the hash key). So, we create uuid for userid.
7+
;; I guess the same applies to Azure Table Storage as well so using uuid here as RowKey.
8+
(defn uuid
9+
[]
10+
(.toString (java.util.UUID/randomUUID)))
11+
12+
(defn get-initial-users
13+
[]
14+
(let [data-dir (get-in ss-config/config [:single-node-data :data-dir])
15+
raw-users (try
16+
(with-open [reader (io/reader (str data-dir "/initial-users.csv"))]
17+
(doall
18+
(csv/read-csv reader :separator \tab)))
19+
(catch java.io.FileNotFoundException _ []))]
20+
(reduce (fn [users user]
21+
(assoc users (first user)
22+
{:userid (nth user 0)
23+
:email (nth user 1)
24+
:first-name (nth user 2)
25+
:last-name (nth user 3)
26+
:hashed-password (nth user 4)})) {} raw-users)))

0 commit comments

Comments
 (0)