;; This Source Code Form is subject to the terms of the Mozilla Public ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; ;; Copyright (c) KALEIDOS SUBSIDIARY SL (ns frontend-tests.data.dashboard-test (:require [app.common.uuid :as uuid] [app.config :as cf] [app.main.data.common :as dcm] [app.main.data.dashboard :as dd] [app.main.data.notifications :as ntf] [app.main.data.websocket :as dws] [app.main.repo :as rp] [app.main.router :as rt] [app.util.i18n :as i18n] [beicon.v2.core :as rx] [cljs.test :as t :include-macros true] [frontend-tests.helpers.async :as async] [frontend-tests.helpers.mock :as mock] [potok.v2.core :as ptk])) (defn- tr-args-for-team-org-toast "Runs handle-change-team-organization and returns recorded i18n/tr calls." [notification organization] (let [team-id (uuid/next) current-url (str "https://penpot.example.com/#/dashboard/recent?team-id=" team-id) state {:current-team-id team-id :teams {team-id {:id team-id :name "Team-Apple"}}} event (dcm/handle-change-team-organization {:team {:id team-id :organization organization} :notification notification}) tr-calls (atom [])] (-> (mock/with-mocks* {cf/flags (conj cf/flags :admin-console) ;; plain variadic fn: mock/stub does not cover IFn variadic dispatch i18n/tr (fn [key & args] (swap! tr-calls conj [key (vec args)]) (str key ":" (pr-str args))) rp/cmd! (mock/stub (fn [cmd params] (if (= [:check-nitrate-sso {:team-id team-id :url current-url}] [cmd params]) (rx/of {:authorized true}) (rx/throw (ex-info "unexpected RPC" {:cmd cmd :params params}))))) rt/get-current-href (mock/stub (constantly current-url))} (await (async/observe (ptk/watch event state nil) :on-next (fn [ev] (t/is (= ::ntf/show (ptk/type ev))))))) (.then (fn [_] @tr-calls))))) (t/deftest ^:async team-org-change-toast-names-team-when-joining-org (let [tr-calls (await (tr-args-for-team-org-toast "dashboard.team-belong-organization" {:id (uuid/next) :name "OrgB"}))] (t/is (= [["dashboard.team-belong-organization" ["Team-Apple" "OrgB"]]] tr-calls)))) (t/deftest ^:async team-org-change-toast-names-team-when-leaving-org (let [tr-calls (await (tr-args-for-team-org-toast "dashboard.team-no-longer-belong-organization" {:name "OrgA"}))] (t/is (= [["dashboard.team-no-longer-belong-organization" ["Team-Apple" "OrgA"]]] tr-calls)))) (t/deftest ^:async duplicated-project-has-a-file-count-and-is-unpinned (let [id (uuid/next) copy-id (uuid/next) team-id (uuid/next) copied {:id copy-id :team-id team-id :name "Copy"} state {:projects {id {:id id :name "Source" :count 9 :is-pinned true}}} result (atom state)] (await (mock/with-mocks* {rp/cmd! (mock/stub (fn [cmd _] (->> (rx/of (case cmd :duplicate-project copied :get-project-files [{:id (uuid/next)} {:id (uuid/next)}])) (rx/observe-on :async))))} (await (async/observe (ptk/watch (dd/duplicate-project {:id id :name "Source"}) state (rx/empty)) :on-next #(swap! result (partial ptk/update %)))))) (t/is (= (assoc copied :count 2 :is-pinned false) (get-in @result [:projects copy-id]))))) (t/deftest moving-current-team-into-sso-organization-redirects (t/async done (let [team-id (uuid/next) organization {:id (uuid/next) :name "OrgA"} current-url (str "https://penpot.example.com/#/dashboard/recent?team-id=" team-id) redirect-url "https://idp.example.com/authorize" state {:current-team-id team-id} event (dcm/handle-change-team-organization {:team {:id team-id :organization organization} :notification nil})] (mock/with-mocks {cf/flags (conj cf/flags :admin-console) rp/cmd! (mock/stub (fn [cmd params] (if (= [:check-nitrate-sso {:team-id team-id :url current-url}] [cmd params]) (rx/of {:authorized false :redirect-uri redirect-url}) (rx/throw (ex-info "unexpected RPC" {:cmd cmd :params params}))))) rt/get-current-href (mock/stub (constantly current-url))} (fn [done'] (->> (ptk/watch event state nil) (rx/reduce conj []) (rx/subs! (fn [events] (t/is (= [::rt/nav-raw] (mapv ptk/type events)))) (fn [error] (t/is false (str "unexpected error: " error)) (done')) (fn [] (done'))))) done)))) (t/deftest organization-sso-activation-redirects-current-team (t/async done (let [team-id (uuid/next) organization-id (uuid/next) current-url (str "https://penpot.example.com/#/workspace?team-id=" team-id) redirect-url "https://idp.example.com/authorize" state {:current-team-id team-id :teams {team-id {:id team-id :organization {:id organization-id}}}} event (dcm/handle-organization-change-sso {:organization-id organization-id})] (mock/with-mocks {cf/flags (conj cf/flags :admin-console) rp/cmd! (mock/stub (fn [cmd params] (if (= [:check-nitrate-sso {:team-id team-id :url current-url}] [cmd params]) (rx/of {:authorized false :redirect-uri redirect-url}) (rx/throw (ex-info "unexpected RPC" {:cmd cmd :params params}))))) rt/get-current-href (mock/stub (constantly current-url))} (fn [done'] (->> (ptk/watch event state nil) (rx/reduce conj []) (rx/subs! (fn [events] (t/is (= [::rt/nav-raw] (mapv ptk/type events)))) (fn [error] (t/is false (str "unexpected error: " error)) (done')) (fn [] (done'))))) done)))) (defn- sent-messages "Runs the watch of `event` against `stream` with `dws/send` stubbed and resolves to the messages it sends over the websocket." [event state stream] (let [sent (atom [])] (-> (mock/with-mocks* {dws/send (mock/stub (fn [msg] (swap! sent conj msg) msg))} (await (async/observe (ptk/watch event state stream)))) (.then (fn [_] @sent))))) (t/deftest ^:async dashboard-initialize-subscribes-to-team (let [team-id (uuid/next) state {:profile-id (uuid/next) :teams {team-id {:id team-id :organization {:id (uuid/next)}}}} sent (await (sent-messages (dd/initialize team-id) state (rx/empty)))] (t/is (= [{:type :subscribe-team :team-id team-id}] sent)))) (t/deftest ^:async dashboard-initialize-resubscribes-on-reconnect (let [team-id (uuid/next) state {:profile-id (uuid/next) :teams {team-id {:id team-id}}} stream (rx/of (ptk/data-event ::dws/opened {}) (ptk/data-event ::dws/opened {})) sent (await (sent-messages (dd/initialize team-id) state stream))] (t/is (= 3 (count sent))) (t/is (every? #(= {:type :subscribe-team :team-id team-id} %) sent)))) (t/deftest ^:async dashboard-initialize-accepts-team-organization-messages (let [team-id (uuid/next) org-id (uuid/next) state {:profile-id (uuid/next) :teams {team-id {:id team-id :organization {:id org-id}}}} message (fn [topic] (ptk/data-event ::dws/message {:type :organization-change-sso :topic topic :organization-id org-id})) stream (rx/of (message org-id) (message (uuid/next))) processed (atom [])] (await (mock/with-mocks* {dws/send (mock/stub identity)} (await (async/observe (ptk/watch (dd/initialize team-id) state stream) :on-next #(swap! processed conj (ptk/type %)))))) (t/is (= 1 (count (filter #{::dcm/handle-organization-change-sso} @processed))))))