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.As you can see above, the
defn
macro interns the symbolcred-fn
in the current namespace and binds it to a Var referencing your dummy function.Here's the important piece. At compile time, we thread
app-routes
through a series of functions. One of these functions isfriend/authenticate
, which takes a map with key:workflows
. The value of:workflows
is a vector populated with the results of a call tojson-auth/json-login
, which receivesauth/credential-fn
as a parameter. Remember, we are inside a def, so this is all happening at compile time. We look up the symbolcred-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.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 ofauth/cred-fn
has already been captured, when wedef
'dapp
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.)
And in your tests...
This works because, at compile time, the value of
auth/cred-fn
that gets captured is a function that simply delegates toauth/another-fn
. Note thatauth/another-fn
has not been evaluated yet. Now in our tests, Midje rebindsauth/another-fn
to reference the mock. Then it executes the post, and somewhere in the middle-ware,auth/cred-fn
gets invoked. Insideauth/cred-fn
, we look up the Var bound toauth/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