I'm trying to test my routing in isolation using Midje. For some routes that hit the database I have no trouble using (provided ...)
to isolate the route from a real db call. I've introduced Friend for authentication and I've been unable to fake the call to the credential function.
My credential function looks like this (It's implemented like this because I don't want it getting called just yet):
(defn cred-fn
[creds]
(println (str "hey look I got called with " creds))
(throw (Exception.)))
The middleware for the routes then look like this:
(def app
(-> app-routes
(wrap-json-body {:keywords? true :bigdecimals? true})
wrap-json-response
(wrap-defaults defaults)
(friend/authenticate
{:unauthorized-handler json-auth/login-failed
:workflows [(json-auth/json-login
:login-uri "/login"
:login-failure-handler json-auth/login-failed
:credential-fn auth/cred-fn)]})
(ring-session/wrap-session)))
I've also tried without using the auth-json-workflow, the implementation for the routes looks almost identical and I can add that if it helps but I get the same result.
And then my tests look like this (using ring-mock):
(defn post [url body]
(-> (mock/request :post url body)
(mock/content-type "application/json")
app))
(fact "login with incorrect username and password returns unauthenticated"
(:status (post "/login" invalid-auth-account-json)) => 401
(provided
(auth/cred-fn anything) => nil))
(fact "login with correct username and password returns success"
(:status (post "/login" auth-account-json)) => 200
(provided
(auth/cred-fn anything) => {:identity "root"}))
I then get the following output running the tests:
hey look I got called with {:password "admin_password", :username "not-a-user"}
FAIL at (handler.clj:66)
These calls were not made the right number of times:
(auth/cred-fn anything) [expected at least once, actually never called]
FAIL "routes - authenticated routes - login with incorrect username and password returns unauthenticated" at (handler.clj:64)
Expected: 401
Actual: java.lang.Exception
clojure_api_seed.authentication$cred_fn.invoke(authentication.clj:23)
hey look I got called with {:password "admin_password", :username "root"}
FAIL at (handler.clj:70)
These calls were not made the right number of times:
(auth/cred-fn anything) [expected at least once, actually never called]
FAIL "routes - authenticated routes - login with correct username and password returns success" at (handler.clj:68)
Expected: 200
Actual: java.lang.Exception
clojure_api_seed.authentication$cred_fn.invoke(authentication.clj:23)
So from what I can see the provided statement is not taking effect, and I'm not sure why. Any ideas?
I recently ran into a similar issue, and after some digging I
think I understand why this is happening. Let's take a look at how
the bindings for auth/cred-fn
change over time.
(clojure.pprint/pprint (macroexpand '(defn cred-fn
[creds]
(println (str "hey look I got called with " creds))
(throw (Exception.)))))
(def
cred-fn
(clojure.core/fn
([creds]
(println (str "hey look I got called with " creds))
(throw (Exception.)))))
As you can see above, the defn
macro interns the symbol cred-fn
in the current namespace and binds it to a Var referencing
your dummy function.
(def app
(-> app-routes
(wrap-json-body {:keywords? true :bigdecimals? true})
wrap-json-response
(wrap-defaults defaults)
(friend/authenticate
{:unauthorized-handler json-auth/login-failed
:workflows [(json-auth/json-login
:login-uri "/login"
:login-failure-handler json-auth/login-failed
:credential-fn auth/cred-fn)]})
(ring-session/wrap-session)))
Here's the important piece. At compile time, we thread
app-routes
through a series of functions. One of these functions
is friend/authenticate
, which takes a map with key :workflows
.
The value of :workflows
is a vector populated with the results of
a call to json-auth/json-login
, which receives auth/credential-fn
as a parameter. Remember, we are inside a def, so this is all
happening at compile time. We look up the symbol cred-fn
in the
auth namespace, and pass in the Var which the symbol is bound
to. At this point, that's still the dummy implementation.
Presumably, json-auth/json-login
captures this implementation
and sets up a request handler which invokes it.
(fact "login with incorrect username and password returns unauthenticated"
(:status (post "/login" invalid-auth-account-json)) => 401
(provided
(auth/cred-fn anything) => nil))
Now we're at runtime. In our precondition, Midje rebinds the
symbol auth/cred-fn
to a Var that references the mock. But the
value of auth/cred-fn
has already been captured, when we def
'd
app
at compile time.
So how come the workaround you posted works? (This was actually the clue that led me to Eureka moment - thanks for that.)
(defn another-fn []
(println (str "hey look I got called"))
(throw (Exception.)))
(defn cred-fn [creds]
(another-fn))
And in your tests...
(fact "login with incorrect username and password returns unauthenticated"
(:status (post "/login" invalid-auth-account-json)) => 401
(provided
(auth/another-fn) => nil))
(fact "login with correct username and password returns success"
(:status (post "/login" auth-account-json)) => 200
(provided
(auth/another-fn) => {:identity "root"}))
This works because, at compile time, the value of auth/cred-fn
that
gets captured is a function that simply delegates to
auth/another-fn
. Note that auth/another-fn
has not been
evaluated yet. Now in our tests, Midje rebinds auth/another-fn
to reference the mock. Then it executes the post, and somewhere
in the middle-ware, auth/cred-fn
gets invoked. Inside
auth/cred-fn
, we look up the Var bound to auth/another-fn
(which
is our mock), and invoke it. And now of course, the behavior is
exactly as you expected the first time.
The moral of this story is, be careful with the def in Clojure