1
0
Fork 0
mirror of https://github.com/heyarne/airsonic-ui.git synced 2026-05-06 18:33:38 +02:00

Make login-flow more robust and add more tests

This commit is contained in:
Arne Schlüter 2018-07-16 17:20:33 +02:00
commit 852a3193ab
9 changed files with 123 additions and 65 deletions

View file

@ -1,5 +1,4 @@
(ns airsonic-ui.db (ns airsonic-ui.db)
(:require [airsonic-ui.routes :as routes]))
(def default-db (def default-db
{:notifications (sorted-map)}) {:notifications (sorted-map)})

View file

@ -12,6 +12,11 @@
(fn [params] (fn [params]
(apply println params))) (apply println params)))
(defn noop
"An event handler that can be used for clarity; doesn't do anything, but might
give a name to an event"
[cofx _] cofx)
;; --- ;; ---
;; app boot flow ;; app boot flow
;; * restoring a previous session ;; * restoring a previous session
@ -31,10 +36,9 @@
troubles with our router." troubles with our router."
[{:keys [db store]} _] [{:keys [db store]} _]
(let [credentials (:credentials store)] (let [credentials (:credentials store)]
{:db (assoc db :credentials credentials) {:dispatch-n [(if credentials
:dispatch-n [(if credentials
[:init-flow/credentials-found credentials] [:init-flow/credentials-found credentials]
[:init-flow/credentials-missing])] [:init-flow/credentials-not-found])]
:routes/start-routing nil})) :routes/start-routing nil}))
(re-frame/reg-event-fx (re-frame/reg-event-fx
@ -45,19 +49,21 @@
(defn credentials-found [_ [_ {:keys [u p server]}]] (defn credentials-found [_ [_ {:keys [u p server]}]]
{:dispatch [:credentials/verification-request u p server]}) {:dispatch [:credentials/verification-request u p server]})
(re-frame/reg-event-fx (re-frame/reg-event-fx :init-flow/credentials-found credentials-found)
:init-flow/credentials-found credentials-found)
(re-frame/reg-event-fx
:init-flow/credentials-missing
;; we don't do anything special here, it's just for the sake of clarity ;; we don't do anything special here, it's just for the sake of clarity
(fn [_ _] {}))
(defn credentials-not-found
[cofx _]
(assoc-in cofx [:db :credentials] :credentials/not-found))
(re-frame/reg-event-fx :init-flow/credentials-not-found credentials-not-found)
;; --- ;; ---
;; auth logic ;; auth logic
;; --- ;; ---
(defn-traced credentials-verification-request (defn credentials-verification-request
"Tries to authenticate a user by pinging the server with credentials, saving "Tries to authenticate a user by pinging the server with credentials, saving
them when the request was successful. Bypasses the request when a user saved them when the request was successful. Bypasses the request when a user saved
their credentials." their credentials."
@ -66,21 +72,25 @@
:uri (api/url server "ping" {:u user :p pass}) :uri (api/url server "ping" {:u user :p pass})
:response-format (ajax/json-response-format {:keywords? true}) :response-format (ajax/json-response-format {:keywords? true})
:on-success [:credentials/verification-response user pass server] :on-success [:credentials/verification-response user pass server]
:on-failure [:api/bad-response]}}) :on-failure [:credentials/verification-failure]}})
(re-frame/reg-event-fx (re-frame/reg-event-fx :credentials/verification-request credentials-verification-request)
:credentials/verification-request credentials-verification-request)
(defn credentials-verification-response (defn credentials-verification-response
"Since we don't get real status codes, we have to look into the server's "Since we don't get real status codes, we have to look into the server's
response and see whether we actually sent the correct credentials" response and see whether we actually sent the correct credentials"
[fx [_ user pass server response]] [fx [_ user pass server response]]
{:dispatch (if (api/is-error? response) {:dispatch (if (api/is-error? response)
[:notification/show :error (api/error-msg (api/->exception response))] [:credentials/verification-failure response]
[:credentials/verified user pass server])}) [:credentials/verified user pass server])})
(re-frame/reg-event-fx (re-frame/reg-event-fx :credentials/verification-response credentials-verification-response)
:credentials/verification-response credentials-verification-response)
(defn credentials-verification-failure [fx [_ response]]
(-> (assoc-in fx [:db :credentials] :credentials/verification-failure)
(assoc :dispatch [:notification/show :error (api/error-msg (api/->exception response))])))
(re-frame/reg-event-fx :credentials/verification-failure credentials-verification-failure)
(defn credentials-verified (defn credentials-verified
"Gets called after the server indicates that the credentials entered by a user "Gets called after the server indicates that the credentials entered by a user
@ -92,8 +102,7 @@
:db (assoc db :credentials credentials) :db (assoc db :credentials credentials)
:dispatch [::logged-in]})) :dispatch [::logged-in]}))
(re-frame/reg-event-fx (re-frame/reg-event-fx :credentials/verified credentials-verified)
:credentials/verified credentials-verified)
;; TODO: We have to find another solution for this once we have routes that ;; TODO: We have to find another solution for this once we have routes that
;; don't require a login but have the bottom controls ;; don't require a login but have the bottom controls
@ -103,10 +112,10 @@
(fn [_] (fn [_]
(.. js/document -documentElement -classList (add "has-navbar-fixed-bottom")))) (.. js/document -documentElement -classList (add "has-navbar-fixed-bottom"))))
(defn logged-in (defn logged-in
[cofx _] [cofx _]
(let [redirect (or (get-in cofx [:routes/from-query-param :redirect]) (let [redirect (or (get-in cofx [:routes/from-query-param :redirect]) [::routes/main])]
[::routes/main])]
{:routes/navigate redirect {:routes/navigate redirect
:show-nav-bar nil})) :show-nav-bar nil}))
@ -117,17 +126,16 @@
(defn logout (defn logout
"Clears all credentials and redirects the user to the login page" "Clears all credentials and redirects the user to the login page"
[_ [_ & args]] [cofx [_ & args]]
(let [args (apply hash-map args)] (let [args (apply hash-map args)]
{:routes/navigate (if-let [redirect (:redirect-to args)] {:routes/navigate (if-let [redirect (:redirect-to args)]
[::routes/login {} {:redirect (routes/encode-route redirect)}] [::routes/login {} {:redirect (routes/encode-route redirect)}]
[::routes/login]) [::routes/login])
:routes/unset-credentials nil :routes/unset-credentials nil
:store nil :store nil
:db db/default-db})) :db (merge (:db cofx) db/default-db {:credentials :credentials/logged-out})}))
(re-frame/reg-event-fx (re-frame/reg-event-fx ::logout logout)
::logout logout)
;; --- ;; ---
;; api interaction ;; api interaction
@ -144,8 +152,7 @@
:on-success [:api/good-response] :on-success [:api/good-response]
:on-failure [:api/bad-response]}}) :on-failure [:api/bad-response]}})
(re-frame/reg-event-fx (re-frame/reg-event-fx :api/request api-request)
:api/request api-request)
(defn good-api-response [fx [_ response]] (defn good-api-response [fx [_ response]]
(try (try
@ -153,15 +160,13 @@
(catch ExceptionInfo e (catch ExceptionInfo e
{:dispatch [:notification/show :error (api/error-msg e)]}))) {:dispatch [:notification/show :error (api/error-msg e)]})))
(re-frame/reg-event-fx (re-frame/reg-event-fx :api/good-response good-api-response)
:api/good-response good-api-response)
(defn bad-api-response [db event] (defn bad-api-response [db event]
{:log ["API call gone bad; are CORS headers missing? check for :status 0" event] {:log ["API call gone bad; are CORS headers missing? check for :status 0" event]
:dispatch [:notification/show :error "Communication with server failed. Check browser logs for details."]}) :dispatch [:notification/show :error "Communication with server failed. Check browser logs for details."]})
(re-frame/reg-event-fx (re-frame/reg-event-fx :api/bad-response bad-api-response)
:api/bad-response bad-api-response)
;; --- ;; ---
;; musique ;; musique
@ -256,12 +261,10 @@
:message message}) :message message})
(assoc :dispatch-later (hide-later level)))))) (assoc :dispatch-later (hide-later level))))))
(re-frame/reg-event-fx (re-frame/reg-event-fx :notification/show show-notification)
:notification/show show-notification)
(defn hide-notification (defn hide-notification
[db [_ notification-id]] [db [_ notification-id]]
(update db :notifications dissoc notification-id)) (update db :notifications dissoc notification-id))
(re-frame/reg-event-db (re-frame/reg-event-db :notification/hide hide-notification)
:notification/hide hide-notification)

View file

@ -100,7 +100,7 @@
;; this allows us to encode a complete route in a url fragment; useful for ;; this allows us to encode a complete route in a url fragment; useful for
;; doing redirects ;; doing redirects
(let [[_ _ query] (current-route) (let [[_ _ query] (current-route)
from-param (decode-route (get query param))] from-param (some-> (get query param) (decode-route))]
(assoc-in coeffects [:routes/from-query-param param] from-param)))) (assoc-in coeffects [:routes/from-query-param param] from-param))))
(defn start-routing! (defn start-routing!

View file

@ -2,6 +2,14 @@
(:require [re-frame.core :as re-frame :refer [subscribe]] (:require [re-frame.core :as re-frame :refer [subscribe]]
[airsonic-ui.utils.api :as api])) [airsonic-ui.utils.api :as api]))
(defn is-booting?
"Predicate to tell whether our app is still in the process of initialization"
[{:keys [credentials]} _]
(and (not (map? credentials))
(not (#{:credentials/not-found :credentials/verification-failure :credentials/logged-out} credentials))))
(re-frame/reg-sub ::is-booting? is-booting?)
;; can be used to query the user's credentials ;; can be used to query the user's credentials
(re-frame/reg-sub (re-frame/reg-sub

View file

@ -68,10 +68,11 @@
[bottom-bar]]))) [bottom-bar]])))
(defn main-panel [] (defn main-panel []
(let [[route params query] @(subscribe [::subs/current-route]) (let [notifications @(subscribe [::subs/notifications])
notifications @(subscribe [::subs/notifications])] is-booting? @(subscribe [::subs/is-booting?])
[route params query] @(subscribe [::subs/current-route])]
[:div [:div
[notification-list notifications] [notification-list notifications]
(if route (if is-booting?
[app route params query] [:div.app-loading>div.loader]
[:div.app-loading>div.loader])])) [app route params query] )]))

View file

@ -13,18 +13,17 @@
(letfn [(no-previous-session [] (letfn [(no-previous-session []
(events/restore-previous-session {} [:_])) (events/restore-previous-session {} [:_]))
(has-previous-session [] (has-previous-session []
(events/restore-previous-session {:store {:u "test" (events/restore-previous-session {:store {:credentials {:u "test"
:p "test" :p "test"
:server "https://demo.airsonic.io/"}} [:_]))] :server "https://demo.airsonic.io/"}}} [:_]))]
(testing "Should initialize routing after checking for previous credentials" (testing "Should initialize routing after checking for previous credentials"
(is (contains? (no-previous-session) :routes/start-routing)) (is (contains? (no-previous-session) :routes/start-routing))
(is (contains? (has-previous-session) :routes/start-routing))) (is (contains? (has-previous-session) :routes/start-routing)))
(testing "Should indicate success or failure" (testing "Should indicate success or failure"
(is (dispatches? (no-previous-session)) :init-flow/credentials-missing) (is (true? (dispatches? (no-previous-session) :init-flow/credentials-not-found)))
(is (dispatches? (has-previous-session)) :init-flow/credentials-found)) (is (true? (dispatches? (has-previous-session) :init-flow/credentials-found))))
(testing "Should send an auth request on success" (testing "Should send an auth request on success"
(is (dispatches? (events/credentials-found {} [:_]) :credentials/verification-request))) (is (true? (dispatches? (events/credentials-found {} [:_]) :credentials/verification-request))))))
(testing "Should redirect to the login form when there's no previous session to be restored")))
(deftest authentication (deftest authentication
(testing "Server ping for verifications" (testing "Server ping for verifications"
@ -32,20 +31,23 @@
fx (events/credentials-verification-request {} [:_ "user" "pass" server]) fx (events/credentials-verification-request {} [:_ "user" "pass" server])
request (:http-xhrio fx)] request (:http-xhrio fx)]
(testing "uses correct server url" (testing "uses correct server url"
(is (str/starts-with? (:uri request) server)) (let [uri (:uri request)]
(is (str/includes? (:uri request) "/ping")) (is (true? (str/starts-with? uri server)))
(is (str/includes? (:uri request) "p=pass")) (is (true? (str/includes? uri "/ping")))
(is (str/includes? (:uri request) "u=user"))) (is (true? (str/includes? uri "p=pass")))
(is (true? (str/includes? uri "u=user")))))
(testing "invokes correct success callback" (testing "invokes correct success callback"
(is (= :credentials/verification-response (first (:on-success request))))))) (is (= :credentials/verification-response (first (:on-success request)))))))
(testing "Auth response verification" (testing "Auth response"
(let [server "https://localhost" (testing "verification for bad responses"
fx (events/credentials-verification-response {} [:_ "user" "pass" server (:error responses)])] (let [ev [:_ "user" "pass" "https://localhost"]
(is (= (dispatches? fx :notification/show)) invalid-credentials (events/credentials-verification-response {} (conj ev (:auth-failure responses)))
"shows an error when we have a bad response")) verification-failure (events/credentials-verification-failure {} [:_ (:auth-failure responses)])]
(is (true? (dispatches? invalid-credentials :credentials/verification-failure)) "fails for bad responses")
(is (true? (dispatches? verification-failure :notification/show)) "shows the failure the the user")))
(let [server "https://localhost" (let [server "https://localhost"
fx (events/credentials-verification-response {} [:_ "username" "password" server (:auth-success responses)])] fx (events/credentials-verification-response {} [:_ "username" "password" server (:auth-success responses)])]
(is (dispatches? fx [:credentials/verified "username" "password" server])))) (is (true? (dispatches? fx [:credentials/verified "username" "password" server])))))
(testing "On succesful response" (testing "On succesful response"
(let [credentials {:u "user" :p "pass" :server "https://localhost"} (let [credentials {:u "user" :p "pass" :server "https://localhost"}
fx (events/credentials-verified {} [:_ (:u credentials) (:p credentials) (:server credentials)])] fx (events/credentials-verified {} [:_ (:u credentials) (:p credentials) (:server credentials)])]
@ -54,7 +56,7 @@
(testing "credentials are saved in the global state" (testing "credentials are saved in the global state"
(is (= credentials (get-in fx [:db :credentials])))) (is (= credentials (get-in fx [:db :credentials]))))
(testing "the login process is finalized" (testing "the login process is finalized"
(is (dispatches? fx ::events/logged-in)))))) (is (true? (dispatches? fx ::events/logged-in)))))))
(deftest logout (deftest logout
(let [fx (events/logout {} [:_])] (let [fx (events/logout {} [:_])]
@ -65,7 +67,7 @@
(testing "Should unset authentication in the router" (testing "Should unset authentication in the router"
(is (contains? fx :routes/unset-credentials))) (is (contains? fx :routes/unset-credentials)))
(testing "Should reset the app-db" (testing "Should reset the app-db"
(is (= db/default-db (:db fx))))) (is (= (every? #(= (get db/default-db %) (get-in fx [:db %])) (keys db/default-db))))))
(testing "Should be able to keep a redirection parameter" (testing "Should be able to keep a redirection parameter"
(let [redirect [:route {:with-data #{1 2 3 4 5}}] (let [redirect [:route {:with-data #{1 2 3 4 5}}]
fx (events/logout {} [:_ :redirect-to redirect])] fx (events/logout {} [:_ :redirect-to redirect])]

View file

@ -1,8 +1,8 @@
(ns airsonic-ui.fixtures) (ns airsonic-ui.fixtures)
(def responses {:error {:subsonic-response (def responses {:error {:subsonic-response
{:error {:code 40 {:error {:code 50
:message "Wrong username or password"} :message "Incompatible Airsonic REST protocol version. Server must upgrade."}
:status "failed" :status "failed"
:version "1.15.0"}} :version "1.15.0"}}
:ok {:subsonic-response :ok {:subsonic-response
@ -11,7 +11,11 @@
:status "ok" :status "ok"
:version "1.15.0"}} :version "1.15.0"}}
:auth-success {:subsonic-response {:status "ok" :auth-success {:subsonic-response {:status "ok"
:version "1.15.0"}}}) :version "1.15.0"}}
:auth-failure {:subsonic-response {:status "failed"
:version "1.15.0"
:error {:code 40
:message "Wrong username or password."}}}})
(def song (def song
{:artistId 42, {:artistId 42,

View file

@ -1,9 +1,42 @@
(ns airsonic-ui.subs-test (ns airsonic-ui.subs-test
(:require [cljs.test :refer [deftest testing is]] (:require [cljs.test :refer [deftest testing is]]
[airsonic-ui.fixtures :refer [song]] [airsonic-ui.db :as db]
[airsonic-ui.fixtures :refer [song] :as fixtures]
[airsonic-ui.utils.api :as api] [airsonic-ui.utils.api :as api]
[airsonic-ui.events :as ev]
[airsonic-ui.subs :as subs])) [airsonic-ui.subs :as subs]))
(def creds {:credentials {:u "test"
:p "test"
:server "https://demo.airsonic.io/"}})
(deftest is-booting
(testing "Should be true when provided the initial state"
(is (true? (subs/is-booting? db/default-db [:_]))))
(testing "Should be true when we have credentials but no response yet"
(is (true? (-> (ev/restore-previous-session {:store creds} [:_])
(ev/credentials-found [:_])
:db
(subs/is-booting? [:_])))))
(testing "Should be false when the login screen is shown"
(is (false? (-> (ev/restore-previous-session {} [:_])
(ev/credentials-not-found [:_])
:db
(subs/is-booting? [:_])))))
(let [{:keys [u p server]} (:credentials creds)]
(testing "Should be false after we verified our credentials with the server"
(is (false? (-> (ev/credentials-verified {:db {}} [:_ u p server])
:db
(subs/is-booting? [:_])))))
(testing "Should be false after the server rejected our credentials"
(is (false? (-> (ev/credentials-verification-failure {} [:_ (:auth-failure fixtures/responses)])
:db
(subs/is-booting? [:_]))))))
(testing "Should be false when a user logged out voluntarily"
(is (false? (-> (ev/logout {} [:_])
:db
(subs/is-booting? [:_]))))))
(deftest cover-images (deftest cover-images
(let [credentials {:server "https://foo.bar" (let [credentials {:server "https://foo.bar"
:u "test-user" :u "test-user"

View file

@ -47,3 +47,11 @@
(api/unwrap-response error-response) (api/unwrap-response error-response)
(catch ExceptionInfo e (catch ExceptionInfo e
(= (:error error-response) (ex-data e))))))) (= (:error error-response) (ex-data e)))))))
(deftest error-recognition
(testing "Should detect error responses"
(is (true? (api/is-error? (:error responses))))
(is (true? (api/is-error? (:auth-failure responses)))))
(testing "Should pass on good responses"
(is (false? (api/is-error? (:ok responses))))
(is (false? (api/is-error? (:auth-success responses))))))