Подделка учетных данных друга с помощью Midje

Я пытаюсь проверить свою маршрутизацию изолированно, используя Midje. Для некоторых маршрутов, попадающих в базу данных, у меня нет проблем с использованием (provided ...) для изоляции маршрута от реального вызова базы данных. Я представил Friend для аутентификации, и мне не удалось подделать вызов функции учетных данных.

Моя функция учетных данных выглядит так (она реализована так, потому что я пока не хочу, чтобы она вызывалась):

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

Промежуточное ПО для маршрутов выглядит следующим образом:

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

Я также пробовал без использования рабочего процесса auth-json, реализация маршрутов выглядит почти идентично, и я могу добавить, если это поможет, но я получаю тот же результат.

И тогда мои тесты выглядят так (с использованием кольцевого макета):

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

Затем я получаю следующий вывод при выполнении тестов:

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)

Итак, из того, что я вижу, предоставленное заявление не вступает в силу, и я не уверен, почему. Есть идеи?


person Hugo    schedule 14.11.2014    source источник
comment
Возможно, вы столкнулись с проблемой, когда midje применяет замену, ср. stackoverflow.com/questions/21042577/ Я не знаю, когда/где именно будет вызываться ваш cred-fn.   -  person schaueho    schedule 14.11.2014
comment
Предоставленное работает, и я нашел обходной путь, который я разместил ниже.   -  person Hugo    schedule 14.11.2014
comment
Midje не издевается над макросами, может быть проблема в этом.   -  person fiso    schedule 02.12.2014


Ответы (2)


Недавно я столкнулся с подобной проблемой, и после некоторого копания я думаю, что понял, почему это происходит. Давайте посмотрим, как со временем меняются привязки для auth/cred-fn.

(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.)))))

Как вы можете видеть выше, макрос defn вставляет символ cred-fn в текущее пространство имен и связывает его с Var, ссылающимся на вашу фиктивную функцию.

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

Вот важная часть. Во время компиляции мы пропускаем app-routes через серию функций. Одной из таких функций является friend/authenticate, которая берет карту с ключом :workflows. Значение :workflows представляет собой вектор, заполненный результатами вызова json-auth/json-login, который получает auth/credential-fn в качестве параметра. Помните, что мы находимся внутри определения, так что все это происходит во время компиляции. Мы ищем символ cred-fn в пространстве имен auth и передаем Var, к которому привязан символ. На данный момент это все еще фиктивная реализация. Предположительно, json-auth/json-login фиксирует эту реализацию и устанавливает обработчик запросов, который ее вызывает.

(fact "login with incorrect username and password returns unauthenticated"
    (:status (post "/login" invalid-auth-account-json)) => 401
    (provided
    (auth/cred-fn anything) => nil))

Теперь мы во время выполнения. В нашем предварительном условии Midje перепривязывает символ auth/cred-fn к Var, который ссылается на макет. Но значение auth/cred-fn уже было захвачено, когда мы def'd app во время компиляции.

Так почему же обходной путь, который вы опубликовали, работает? (На самом деле это была подсказка, которая привела меня к моменту Эврики — спасибо за это.)

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

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

И в ваших тестах...

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

Это работает, потому что во время компиляции значение auth/cred-fn, которое перехватывается, является функцией, которая просто делегирует auth/another-fn. Обратите внимание, что auth/another-fn еще не оценивался. Теперь в наших тестах Мидже перепривязывает auth/another-fn для ссылки на макет. Затем он выполняет сообщение, и где-то в промежуточном программном обеспечении вызывается auth/cred-fn. Внутри auth/cred-fn мы ищем Var, привязанный к auth/another-fn (который является нашим макетом), и вызываем его. И теперь, конечно, поведение именно такое, как вы ожидали в первый раз.

Мораль этой истории такова: будьте осторожнее с определением в Clojure

person tronbabylove    schedule 02.05.2015
comment
Вот наглядный пример: gist.github.com/astahlman/b62b95ab62e48e338246 - person tronbabylove; 07.05.2015
comment
Спасибо тебе за это! Мой обходной путь был достаточно хорош, чтобы поддерживать меня, но я рад, что знаю, почему это происходит!! - person Hugo; 30.06.2015

Я до сих пор не уверен, почему это происходит, но у меня есть обходной путь. Если я заменю свой credential-fn на:

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

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

А затем создайте подделку для новой функции в тесте, например:

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

Я получаю результат, которого ожидал. cred-fn по-прежнему вызывается, но another-fn не вызывается из-за ошибки provided.

Если кто-нибудь знает, почему это так, мне было бы интересно узнать. Это может быть связано с тем, как вызывается функция учетных данных? - https://github.com/marianoguerra/friend-json-workflow/blob/master/src/marianoguerra/friend_json_workflow.clj#L46

person Hugo    schedule 14.11.2014