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

Move navigation to interceptor

Squashed commit of the following:

commit c8bf5e0cb4fd95935e06dc46dda38256f5bb970f
Author: Arne Schlüter <arne@schlueter.is>
Date:   Wed Aug 1 11:37:43 2018 +0200

    Start credential verification only if there are previous credentials

commit 61e6f2e7f2fb4d01e59c71c5980b1b761fa0bf83
Author: Arne Schlüter <arne@schlueter.is>
Date:   Wed Aug 1 10:22:31 2018 +0200

    Make `dispatches?` helper return a boolean

commit 4dc10acd5f1eae616d62c24e3cb9685e4e595f04
Author: Arne Schlüter <arne@schlueter.is>
Date:   Wed Aug 1 09:19:49 2018 +0200

    Add joker for linting

commit 7069febff0ed49be5c60e6787bfc9dc5b758917b
Author: Arne Schlüter <arne@schlueter.is>
Date:   Tue Jul 31 14:17:41 2018 +0200

    Implement navigation as interceptor

    FIXME: Unauthorized access doesn't redirect to `#/login?redirect=...`

commit 60f9f03dd86f48234133e76dd57c067afb7a74d4
Author: Arne Schlüter <arne@schlueter.is>
Date:   Wed Jul 18 19:35:47 2018 +0200

    Make booting explicit and prepare for :navigate interceptor
This commit is contained in:
Arne Schlüter 2018-08-01 11:39:24 +02:00
commit 727d454871
14 changed files with 257 additions and 221 deletions

1
.joker Normal file
View file

@ -0,0 +1 @@
{:known-macros [cljs.test/deftest]}

View file

@ -6,7 +6,6 @@
[akiroz.re-frame.storage :as storage] [akiroz.re-frame.storage :as storage]
;; our app ;; our app
[airsonic-ui.audio] ; <- just registers effects here [airsonic-ui.audio] ; <- just registers effects here
[airsonic-ui.routes :as routes]
[airsonic-ui.events :as events] [airsonic-ui.events :as events]
[airsonic-ui.views :as views] [airsonic-ui.views :as views]
[airsonic-ui.config :as config])) [airsonic-ui.config :as config]))

View file

@ -1,4 +1,5 @@
(ns airsonic-ui.db) (ns airsonic-ui.db)
(def default-db (def default-db
{:notifications (sorted-map)}) {:is-booting? true
:notifications (sorted-map)})

View file

@ -24,85 +24,83 @@
;; * sending out the appropriate requests ;; * sending out the appropriate requests
;; --- ;; ---
(defn initialize-app
[{{:keys [credentials]} :store} _]
(let [effects {:db db/default-db
:routes/start-routing nil}]
(if (not (empty? credentials))
(assoc effects :dispatch [:credentials/verify credentials])
effects)))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::initialize-app ::initialize-app
(fn [_]
{:db db/default-db
:dispatch [:init-flow/restore-previous-session]}))
(defn restore-previous-session
"See comment above for different steps; what's important here is that we check
for a previous session before anything else, otherwise we might run into auth
troubles with our router."
[{:keys [db store]} _]
(let [credentials (:credentials store)]
{:dispatch-n [(if credentials
[:init-flow/credentials-found credentials]
[:init-flow/credentials-not-found])]
:routes/start-routing nil}))
(re-frame/reg-event-fx
:init-flow/restore-previous-session
[(re-frame/inject-cofx :store)] [(re-frame/inject-cofx :store)]
restore-previous-session) initialize-app)
(defn credentials-found [_ [_ {:keys [u p server]}]] (defn verify-credentials
{:dispatch [:credentials/verification-request u p server]}) "Initializes the whole authentication chain when we have locally stored
credentials that look plausible."
[_ [_ credentials]]
;; TODO: spec this
(if (every? string? ((juxt :u :p :server) credentials))
{:dispatch [:credentials/send-authentication-request credentials]}))
(re-frame/reg-event-fx :init-flow/credentials-found credentials-found) (re-frame/reg-event-fx :credentials/verify verify-credentials)
;; we don't do anything special here, it's just for the sake of clarity
(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 credentials-verification-request (defn user-login
"Gets called after the user clicked on the login button"
[cofx [_ user pass server]]
(let [credentials {:u user, :p pass, :server server, :verified? false}]
(-> (assoc-in cofx [:db :credentials] credentials)
(assoc :dispatch [:credentials/send-authentication-request credentials]))))
(re-frame/reg-event-fx :credentials/user-login user-login)
(defn authentication-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."
[_ [_ user pass server]] [cofx [_ credentials]]
{:http-xhrio {:method :get (assoc cofx :http-xhrio {:method :get
:uri (api/url server "ping" {:u user :p pass}) :uri (api/url (:server credentials) "ping" (select-keys credentials [:u :p]))
: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/authentication-response credentials]
:on-failure [:credentials/verification-failure]}}) :on-failure [:api/bad-response]}))
(re-frame/reg-event-fx :credentials/verification-request credentials-verification-request) (re-frame/reg-event-fx :credentials/send-authentication-request authentication-request)
(defn credentials-verification-response (defn authentication-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 [_ credentials response]]
{:dispatch (if (api/is-error? response) (assoc fx :dispatch (if (api/is-error? response)
[:credentials/verification-failure response] [:credentials/authentication-failure response]
[:credentials/verified user pass server])}) [:credentials/authentication-success (assoc credentials :verified? true)])))
(re-frame/reg-event-fx :credentials/verification-response credentials-verification-response) (re-frame/reg-event-fx :credentials/authentication-response authentication-response)
(defn credentials-verification-failure [fx [_ response]] (defn authentication-failure
(-> (assoc-in fx [:db :credentials] :credentials/verification-failure) "Removes all stored credentials and displays potential api errors to the user"
(assoc :dispatch [:notification/show :error (api/error-msg (api/->exception response))]))) [fx [_ response]]
(-> (assoc fx :dispatch [:notification/show :error (api/error-msg (api/->exception response))])
(update :store dissoc :credentials)
(update :db dissoc :credentials)))
(re-frame/reg-event-fx :credentials/verification-failure credentials-verification-failure) (re-frame/reg-event-fx :credentials/authentication-failure authentication-failure)
(defn credentials-verified (defn authentication-success
"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
are correct (see `credentials-verification-request`)" are correct (see `credentials-verification-request`)"
[{:keys [db]} [_ user pass server]] [{:keys [db]} [_ credentials]]
(let [credentials {:u user :p pass :server server}] {:store {:credentials credentials}
{:routes/set-credentials credentials :db (assoc db :credentials (assoc credentials :verified? true))
:store {:credentials credentials} :dispatch [::logged-in]})
:db (assoc db :credentials credentials)
:dispatch [::logged-in]}))
(re-frame/reg-event-fx :credentials/verified credentials-verified) (re-frame/reg-event-fx :credentials/authentication-success authentication-success)
;; 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
@ -112,11 +110,11 @@
(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]) [::routes/main])] (let [redirect (or (get-in cofx [:routes/from-query-param :redirect])
{:routes/navigate redirect [::routes/main])]
{:dispatch [:routes/do-navigation redirect]
:show-nav-bar nil})) :show-nav-bar nil}))
(re-frame/reg-event-fx (re-frame/reg-event-fx
@ -128,12 +126,12 @@
"Clears all credentials and redirects the user to the login page" "Clears all credentials and redirects the user to the login page"
[cofx [_ & args]] [cofx [_ & args]]
(let [args (apply hash-map args)] (let [args (apply hash-map args)]
{:routes/navigate (if-let [redirect (:redirect-to args)] {:dispatch [:routes/do-navigation (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
:store nil :store nil
:db (merge (:db cofx) db/default-db {:credentials :credentials/logged-out})})) :db (-> (merge (:db cofx) db/default-db)
(dissoc :credentials))}))
(re-frame/reg-event-fx ::logout logout) (re-frame/reg-event-fx ::logout logout)
@ -223,8 +221,9 @@
;; --- ;; ---
(re-frame/reg-event-fx (re-frame/reg-event-fx
:routes/navigation :routes/did-navigate
(fn [{:keys [db]} [_ route params query]] (fn [{:keys [db]} [_ route params query]]
;; FIXME: This leads to an ugly "unregistered event handler `nil`" error
;; all the naviagation logic is in routes.cljs; all we need to do here ;; all the naviagation logic is in routes.cljs; all we need to do here
;; is say what actually happens once we've navigated succesfully ;; is say what actually happens once we've navigated succesfully
{:db (assoc db :current-route [route params query]) {:db (assoc db :current-route [route params query])

View file

@ -7,7 +7,7 @@
(def router (def router
(r/router [["/" ::login] (r/router [["/" ::login]
["/hello" ::main] ["/main" ::main]
["/artist/:id" ::artist-view] ["/artist/:id" ::artist-view]
["/album/:id" ::album-view]])) ["/album/:id" ::album-view]]))
@ -46,31 +46,39 @@
;; holding credentials, which is necessary to restrict certain routes, and the ;; holding credentials, which is necessary to restrict certain routes, and the
;; last one is used for actual navigation ;; last one is used for actual navigation
(def credentials (atom nil)) ;; the event to initialize navigation is implemented so the coeffect map is
;; returned unaltered, we just need access to the current app database for
;; authentication, which we get with an interceptor
(re-frame/reg-fx (def ^:private credentials (atom nil))
:routes/set-credentials
(fn [credentials']
(reset! credentials credentials')))
(re-frame/reg-fx (def do-navigation
:routes/unset-credentials "An interceptor which performs the navigation after looking up current
(fn [] credentials in the app database"
(reset! credentials nil))) (re-frame.core/->interceptor
:id :routes/do-navigation
:after (fn do-navigation [context]
(let [[_ & [route]] (get-in context [:coeffects :event])
;; because :routes/do-navigation is both an event handler and
;; an interceptor, we know that when handling the event (see
;; below) the credentials aren't altered anymore
credentials'(get-in context [:coeffects :db :credentials])]
(println "calling do-navigation with" route credentials')
(reset! credentials credentials')
(apply r/navigate! router route)
context))))
(re-frame/reg-fx (re-frame/reg-event-fx :routes/do-navigation do-navigation (fn [& _] nil))
:routes/navigate
(fn [[route-id params query]]
(println "calling ::navigate with" route-id params query)
(r/navigate! router route-id params query)))
(defn can-access? [route] (defn can-access? [route]
(or (not (protected-routes route)) @credentials)) (or (not (protected-routes route))
(:verified? @credentials)))
(defn on-navigate (defn on-navigate
[route-id params query] [route-id params query]
(println "on-navigate is called" route-id params query credentials)
(if (can-access? route-id) (if (can-access? route-id)
(re-frame/dispatch [:routes/navigation route-id params query]) (re-frame/dispatch [:routes/did-navigate route-id params query])
(re-frame/dispatch [:routes/unauthorized route-id params query]))) (re-frame/dispatch [:routes/unauthorized route-id params query])))
(defn encode-route (defn encode-route
@ -89,11 +97,13 @@
[] []
(r/match router (subs (.. js/window -location -hash) 1))) (r/match router (subs (.. js/window -location -hash) 1)))
;; add the current route to our coeffect map
(re-frame/reg-cofx (re-frame/reg-cofx
:routes/current-route :routes/current-route
(fn [coeffects _] (fn [coeffects _]
(assoc coeffects :routes/current-route (current-route)))) (assoc coeffects :routes/current-route (current-route))))
;; add route into from a URL parameter to our coeffect map
(re-frame/reg-cofx (re-frame/reg-cofx
:routes/from-query-param :routes/from-query-param
(fn [coeffects param] (fn [coeffects param]

View file

@ -3,19 +3,18 @@
[airsonic-ui.utils.api :as api])) [airsonic-ui.utils.api :as api]))
(defn is-booting? (defn is-booting?
"Predicate to tell whether our app is still in the process of initialization" "The boot process starts with setting up routing and continues if we found
[{:keys [credentials]} _] previous credentials and ends when we receive a response from the server."
(and (not (map? credentials)) [db _]
(not (#{:credentials/not-found :credentials/verification-failure :credentials/logged-out} credentials)))) ;; so either we don't have any credentials or they are not verified
(or (empty? (:current-route db))
(and (not (empty? (:credentials db)))
(not (get-in db [:credentials :verified?])))))
(re-frame/reg-sub ::is-booting? is-booting?) (re-frame/reg-sub ::is-booting? is-booting?)
;; can be used to query the user's credentials (defn credentials [db _] (:credentials db))
(re-frame/reg-sub ::credentials credentials)
(re-frame/reg-sub
::credentials
(fn [db _]
(:credentials db)))
(re-frame/reg-sub (re-frame/reg-sub
::user ::user
@ -65,7 +64,5 @@
;; user notifications ;; user notifications
(re-frame/reg-sub (defn notifications [db _] (:notifications db))
::notifications (re-frame/reg-sub ::notifications notifications)
(fn [db _]
(:notifications db)))

View file

@ -1,12 +1,10 @@
(ns airsonic-ui.views (ns airsonic-ui.views
(:require [re-frame.core :refer [dispatch subscribe]] (:require [re-frame.core :refer [dispatch subscribe]]
[airsonic-ui.config :as config]
[airsonic-ui.routes :as routes :refer [url-for]] [airsonic-ui.routes :as routes :refer [url-for]]
[airsonic-ui.events :as events] [airsonic-ui.events :as events]
[airsonic-ui.subs :as subs] [airsonic-ui.subs :as subs]
[airsonic-ui.views.notifications :refer [notification-list]] [airsonic-ui.views.notifications :refer [notification-list]]
[airsonic-ui.views.loading-spinner :refer [loading-spinner]]
[airsonic-ui.views.breadcrumbs :refer [breadcrumbs]] [airsonic-ui.views.breadcrumbs :refer [breadcrumbs]]
[airsonic-ui.views.bottom-bar :refer [bottom-bar]] [airsonic-ui.views.bottom-bar :refer [bottom-bar]]
[airsonic-ui.views.login :refer [login-form]] [airsonic-ui.views.login :refer [login-form]]
@ -49,30 +47,33 @@
;; putting everything together ;; putting everything together
(defn app [route params query] (defn app [route-id params query]
(let [user @(subscribe [::subs/user]) (let [user @(subscribe [::subs/user])
content @(subscribe [::subs/current-content])] content @(subscribe [::subs/current-content])]
(if (= route ::routes/login) [:div
[login-form] [:main.columns
[:div [:div.column.is-2.sidebar
[:main.columns [sidebar user]]
[:div.column.is-2.sidebar [:div.column
[sidebar user]] [:section.section
[:div.column [breadcrumbs content]
[:section.section (case route-id
[breadcrumbs content] ::routes/main [most-recent content]
(case route ::routes/artist-view [artist-detail content]
::routes/main [most-recent content] ::routes/album-view [album-detail content])]]]
::routes/artist-view [artist-detail content] [bottom-bar]]))
::routes/album-view [album-detail content])]]]
[bottom-bar]])))
(defn main-panel [] (defn main-panel []
(let [notifications @(subscribe [::subs/notifications]) (let [notifications @(subscribe [::subs/notifications])
is-booting? @(subscribe [::subs/is-booting?]) is-booting? @(subscribe [::subs/is-booting?])
[route params query] @(subscribe [::subs/current-route])] [route-id params query] @(subscribe [::subs/current-route])]
(println "route-id" route-id (case route-id
::routes/login "::routes/login"
"something else"))
[:div [:div
[notification-list notifications] [notification-list notifications]
(if is-booting? (if is-booting?
[:div.app-loading>div.loader] [:div.app-loading>div.loader]
[app route params query] )])) (case route-id
::routes/login [login-form]
[app route-id params query]))]))

View file

@ -16,7 +16,7 @@
server (r/atom (.. js/window -location -origin)) server (r/atom (.. js/window -location -origin))
submit (fn [e] submit (fn [e]
(.preventDefault e) (.preventDefault e)
(dispatch [:credentials/verification-request @user @pass @server]))] (dispatch [:credentials/user-login @user @pass @server]))]
(fn [] (fn []
[:section.hero.is-fullheight>div.hero-body [:section.hero.is-fullheight>div.hero-body
[:div.container.has-text-centered>div.column.is-4.is-offset-4 [:div.container.has-text-centered>div.column.is-4.is-offset-4

View file

@ -2,83 +2,112 @@
(:require [cljs.test :refer [deftest testing is]] (:require [cljs.test :refer [deftest testing is]]
[clojure.string :as str] [clojure.string :as str]
[airsonic-ui.test-helpers :refer [dispatches?]] [airsonic-ui.test-helpers :refer [dispatches?]]
[airsonic-ui.fixtures :refer [responses]] [airsonic-ui.fixtures :as fixtures]
[airsonic-ui.db :as db] [airsonic-ui.db :as db]
[airsonic-ui.routes :as routes] [airsonic-ui.routes :as routes]
[airsonic-ui.events :as events])) [airsonic-ui.events :as events]
[airsonic-ui.subs :as subs]))
(enable-console-print!) (enable-console-print!)
(deftest session-restoration ;; the event tests are actually quite nice to write:
(letfn [(no-previous-session [] ;; because everything in re-frame is described as data, we pass on coeffects
(events/restore-previous-session {} [:_])) ;; to event handler after event handler and check if the final coeffect map
(has-previous-session [] ;; looks as expected.
(events/restore-previous-session {:store {:credentials {:u "test"
:p "test"
:server "https://demo.airsonic.io/"}}} [:_]))]
(testing "Should initialize routing after checking for previous credentials"
(is (contains? (no-previous-session) :routes/start-routing))
(is (contains? (has-previous-session) :routes/start-routing)))
(testing "Should indicate success or failure"
(is (true? (dispatches? (no-previous-session) :init-flow/credentials-not-found)))
(is (true? (dispatches? (has-previous-session) :init-flow/credentials-found))))
(testing "Should send an auth request on success"
(is (true? (dispatches? (events/credentials-found {} [:_]) :credentials/verification-request))))))
(deftest authentication (defn no-previous-session [] (events/initialize-app {} [::events/initialize-app]))
(testing "Server ping for verifications" (defn has-previous-session [] (-> {:store {:credentials fixtures/credentials}}
(let [server "https://localhost" (events/initialize-app [::events/initialize-app])))
fx (events/credentials-verification-request {} [:_ "user" "pass" server])
request (:http-xhrio fx)] (deftest app-initialization
(testing "uses correct server url" (testing "Should set up notifications"
(let [uri (:uri request)] (is (map? (subs/notifications (:db (no-previous-session))
(is (true? (str/starts-with? uri server))) [::subs/notifications])))
(is (true? (str/includes? uri "/ping"))) (is (map? (subs/notifications (:db (has-previous-session))
(is (true? (str/includes? uri "p=pass"))) [::subs/notifications]))))
(is (true? (str/includes? uri "u=user"))))) (testing "Should set up the default database")
(testing "invokes correct success callback" (testing "Should initialize credential verification"
(is (= :credentials/verification-response (first (:on-success request))))))) (is (false? (dispatches? (no-previous-session) :credentials/verify)))
(testing "Auth response" (is (true? (dispatches? (has-previous-session) [:credentials/verify fixtures/credentials]))))
(testing "verification for bad responses" (testing "Should initialize the router"
(let [ev [:_ "user" "pass" "https://localhost"] (is (contains? (no-previous-session) :routes/start-routing))
invalid-credentials (events/credentials-verification-response {} (conj ev (:auth-failure responses))) (is (contains? (has-previous-session) :routes/start-routing))))
verification-failure (events/credentials-verification-failure {} [:_ (:auth-failure responses)])]
(is (true? (dispatches? invalid-credentials :credentials/verification-failure)) "fails for bad responses") (deftest credential-verification
(is (true? (dispatches? verification-failure :notification/show)) "shows the failure the the user"))) (testing "Should fail when there are no credentials"
(let [server "https://localhost" (is (false? (dispatches? (-> (no-previous-session)
fx (events/credentials-verification-response {} [:_ "username" "password" server (:auth-success responses)])] (events/verify-credentials [:credentials/verify nil])) [::subs/is-booting?]))))
(is (true? (dispatches? fx [:credentials/verified "username" "password" server]))))) (testing "Should happen server-side when we have credentials"
(testing "On succesful response" (let [cofx (-> (has-previous-session)
(let [credentials {:u "user" :p "pass" :server "https://localhost"} (events/verify-credentials [:credentials/verify fixtures/credentials]))]
fx (events/credentials-verified {} [:_ (:u credentials) (:p credentials) (:server credentials)])] (is (true? (dispatches? cofx :credentials/send-authentication-request)))))
(testing "credentials are sent to the router for access rights" (testing "Should verify the structure of credentials"
(is (= credentials (:routes/set-credentials fx)))) (let [empty-creds {:store {:credentials {}}}]
(testing "credentials are saved in the global state" (is (false? (boolean (dispatches? empty-creds :credentials/send-authentication-request)))))
(is (= credentials (get-in fx [:db :credentials])))) (let [malformed {:store {:credentials {:xyz #{12 34 56}}}}]
(testing "the login process is finalized" (is (false? (boolean (dispatches? malformed :credentials/send-authentication-request)))))))
(is (true? (dispatches? fx ::events/logged-in)))))))
(deftest authentication-request
(let [event [:credentials/send-authentication-request fixtures/credentials]
fx (events/authentication-request {} event)
request (:http-xhrio fx)]
(testing "uses correct server url"
(let [uri (:uri request)]
(is (true? (str/starts-with? uri (:server fixtures/credentials))))
(is (true? (str/includes? uri "/ping")))
(is (true? (str/includes? uri (str "p=" (:p fixtures/credentials)))))
(is (true? (str/includes? uri (str "u=" (:u fixtures/credentials)))))))
(testing "invokes correct callback on server response"
(is (= [:credentials/authentication-response fixtures/credentials] (:on-success request))))
(testing "invokes correct callback when server is not reachable"
(is (= [:api/bad-response] (:on-failure request))))))
(deftest authentication-response
(testing "On success"
(let [cofx (-> (has-previous-session)
(events/authentication-response [:credentials/authentication-response (:auth-success fixtures/responses)])
(events/authentication-success [:credentials/authentication-success]))]
(testing "should mark the credentials as verified"
(is (true? (get-in cofx [:db :credentials :verified?]))))))
(testing "On failure"
(let [cofx (-> (has-previous-session)
(events/authentication-response [:credentials/authentication-response (:auth-failure fixtures/responses)])
(events/authentication-failure [:credentials/authentication-failure (:auth-failure fixtures/responses)]))]
(testing "should display a notification to the user"
(is (true? (dispatches? cofx :notification/show)))))))
(deftest manual-login
(let [{:keys [u p server]} fixtures/credentials
credentials (assoc fixtures/credentials :verified? false)
effect (events/user-login {} [:credentials/user-login u p server])]
(testing "Should save the credentials as unverified"
(is (= credentials (get-in effect [:db :credentials]))))
(testing "Should start the authentication request"
(is (true? (dispatches? effect [:credentials/send-authentication-request credentials]))))))
(deftest logout (deftest logout
(let [fx (events/logout {} [:_])] (let [fx (events/logout {} [:_])]
(testing "Should clear all stored data" (testing "Should clear all stored data"
(is (nil? (:store fx)))) (is (nil? (:store fx))))
(testing "Should redirect to the login screen" (testing "Should redirect to the login screen"
(is (= [::routes/login] (:routes/navigate fx)))) (is (dispatches? fx [:routes/do-navigation [::routes/login]])))
(testing "Should unset authentication in the router"
(is (contains? fx :routes/unset-credentials)))
(testing "Should reset the app-db" (testing "Should reset the app-db"
(is (= (every? #(= (get db/default-db %) (get-in fx [:db %])) (keys db/default-db)))))) (is (= db/default-db (:db fx)))))
(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])] navigation-event (:dispatch (events/logout {} [:_ :redirect-to redirect]))]
(is (= [::routes/login {:redirect redirect}]))))) (is (= :routes/do-navigation (first navigation-event)))
(let [[route-id _ query] (second navigation-event)]
(is (= ::routes/login route-id))
(is (contains? query :redirect))))))
(defn- first-notification [fx] (defn- first-notification [fx]
(-> (get-in fx [:db :notifications]) vals first)) (-> (get-in fx [:db :notifications]) vals first))
(deftest api-interaction (deftest api-interaction
(testing "Should show an error notification when airsonic responds with an error" (testing "Should show an error notification when airsonic responds with an error"
(let [fx (events/good-api-response {} [:_ (:error responses)])] (let [fx (events/good-api-response {} [:_ (:error fixtures/responses)])]
(is (= :error (-> fx :dispatch second)))))) (is (= :error (-> fx :dispatch second))))))
(deftest user-notifications (deftest user-notifications

View file

@ -1,5 +1,9 @@
(ns airsonic-ui.fixtures) (ns airsonic-ui.fixtures)
(def credentials {:u "username"
:p "cleartext-password"
:server "https://demo.airsonic.io"})
(def responses {:error {:subsonic-response (def responses {:error {:subsonic-response
{:error {:code 50 {:error {:code 50
:message "Incompatible Airsonic REST protocol version. Server must upgrade."} :message "Incompatible Airsonic REST protocol version. Server must upgrade."}

View file

@ -5,6 +5,15 @@
(def fixtures (def fixtures
{:default [::route {:some :data} {:some-more true}]}) {:default [::route {:some :data} {:some-more true}]})
#_(deftest permission-checking
(testing "Should succeed for unprotected routes"
(testing "without credentials")
(testing "with unverified credentials"))
(testing "Should fail for protected routes"
(testing "without credentials")
(testing "with unverified credentials"))
(testing "Should succeed for protected routes with verified credentials"))
(deftest route-encoding (deftest route-encoding
(testing "Should return a string with hash-compatible characters" (testing "Should return a string with hash-compatible characters"
(let [encoded (routes/encode-route (:default fixtures))] (let [encoded (routes/encode-route (:default fixtures))]

View file

@ -1,41 +1,27 @@
(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.db :as db] [airsonic-ui.fixtures :as fixtures]
[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" (deftest booting
:p "test" (let [route [:some-route nil nil]
:server "https://demo.airsonic.io/"}}) verified-credentials (assoc fixtures/credentials :verified? true)
is-booting? (fn is-booting? [db]
(deftest is-booting (subs/is-booting? db [:subs/is-booting?]))]
(testing "Should be true when provided the initial state" (testing "Should be false when we don't have previous credentials"
(is (true? (subs/is-booting? db/default-db [:_])))) (is (not (is-booting? {:current-route route})))
(testing "Should be true when we have credentials but no response yet" (is (not (is-booting? {:current-route route
(is (true? (-> (ev/restore-previous-session {:store creds} [:_]) :credentials {}}))) )
(ev/credentials-found [:_]) (testing "Should be true when we have unverified credentials"
:db (is (true? (is-booting? {:current-route route
(subs/is-booting? [:_]))))) :credentials fixtures/credentials}))))
(testing "Should be false when the login screen is shown" (testing "Should be false when we have verified credentials"
(is (false? (-> (ev/restore-previous-session {} [:_]) (is (not (is-booting? {:current-route route
(ev/credentials-not-found [:_]) :credentials verified-credentials}))))
:db (testing "Should be true when routing is not yet set up"
(subs/is-booting? [:_]))))) (is (true? (is-booting? {:current-route nil
(let [{:keys [u p server]} (:credentials creds)] :credentials verified-credentials}))))))
(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"
@ -44,6 +30,6 @@
(testing "Should give the correct path once the credentials are set" (testing "Should give the correct path once the credentials are set"
(is (= (api/cover-url (:server credentials) (is (= (api/cover-url (:server credentials)
(select-keys credentials [:u :p]) (select-keys credentials [:u :p])
song fixtures/song
48) 48)
(subs/cover-url [credentials] [:_ song 48])))))) (subs/cover-url [credentials] [:subs/cover-image fixtures/song 48]))))))

View file

@ -5,4 +5,4 @@
be a whole vector or a keyword which is interpreted as the event name." be a whole vector or a keyword which is interpreted as the event name."
[cofx ev] [cofx ev]
(let [all-events (conj (get cofx :dispatch-n []) (:dispatch cofx))] (let [all-events (conj (get cofx :dispatch-n []) (:dispatch cofx))]
(some #(= ev (if (vector? ev) % (first %))) all-events))) (boolean (some #(= ev (if (vector? ev) % (first %))) all-events))))

View file

@ -4,14 +4,14 @@
(deftest dispatch-helper (deftest dispatch-helper
(testing "single dispatch" (testing "single dispatch"
(is (not (dispatches? {} :foo))) (is (false? (dispatches? {} :foo)))
(is (dispatches? {:dispatch [:foo 1 2 3]} :foo)) (is (true? (dispatches? {:dispatch [:foo 1 2 3]} :foo)))
(is (not (dispatches? {:dispatch [:foo 1 2 3]} :bar))) (is (false? (dispatches? {:dispatch [:foo 1 2 3]} :bar)))
(is (dispatches? {:dispatch [:foo 1 2 3]} [:foo 1 2 3])) (is (true? (dispatches? {:dispatch [:foo 1 2 3]} [:foo 1 2 3])))
(is (not (dispatches? {:dispatch [:foo 1 2 3]} [:bar 2 3])))) (is (false? (dispatches? {:dispatch [:foo 1 2 3]} [:bar 2 3]))))
(testing "multiple dispatch" (testing "multiple dispatch"
(is (not (dispatches? {:dispatch-n [[:bar]]} :foo))) (is (false? (dispatches? {:dispatch-n [[:bar]]} :foo)))
(is (dispatches? {:dispatch-n [[:foo 1 2 3]]} :foo)) (is (true? (dispatches? {:dispatch-n [[:foo 1 2 3]]} :foo)))
(is (not (dispatches? {:dispatch-n [[:foo 1 2 3]]} :bar))) (is (false? (dispatches? {:dispatch-n [[:foo 1 2 3]]} :bar)))
(is (dispatches? {:dispatch-n [[:foo 1 2 3]]} [:foo 1 2 3])) (is (dispatches? {:dispatch-n [[:foo 1 2 3]]} [:foo 1 2 3]))
(is (not (dispatches? {:dispatch-n [[:foo 1 2 3]]} [:bar 2 3]))))) (is (false? (dispatches? {:dispatch-n [[:foo 1 2 3]]} [:bar 2 3])))))