Faking friend credential function using Midje

221 views Asked by At

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?

2

There are 2 answers

2
tronbabylove On BEST ANSWER

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

0
Hugo On

I'm still not sure why this is happening but I have a work around. If I replace my credential-fn with:

(defn another-fn
  []
  (println (str "hey look I got called"))
  (throw (Exception.)))

(defn cred-fn
  [creds]
  (another-fn))

And then create a fake for the new function in the test, like this:

(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"}))

I get the result I was expecting. cred-fn still gets called but another-fn doesn't get called due to the provided.

If anyone knows why this is the case I'd be interested in knowing. It might be due to the way that the credential function gets called? - https://github.com/marianoguerra/friend-json-workflow/blob/master/src/marianoguerra/friend_json_workflow.clj#L46