mirror of
https://github.com/penpot/penpot.git
synced 2026-08-14 00:38:42 +00:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
be9df28b00
@ -367,7 +367,7 @@
|
||||
email (str/lower email)
|
||||
fullname (d/normalize-string (:fullname params))
|
||||
locale (d/normalize-string locale)
|
||||
theme (d/normalize-string theme)
|
||||
theme (some-> theme d/normalize-string not-empty)
|
||||
|
||||
photo-id (some->> (or (:oidc/picture props)
|
||||
(:google/picture props)
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
{
|
||||
"~:iss": "~:verify-email",
|
||||
"~:profile-id": "~uc7ce0794-0992-8105-8004-38e630f29a9b",
|
||||
"~:profile": {
|
||||
"~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b",
|
||||
"~:email": "foo@example.com",
|
||||
"~:fullname": "Princesa Leia",
|
||||
"~:auth-backend": "penpot",
|
||||
"~:is-active": true,
|
||||
"~:is-demo": false,
|
||||
"~:is-muted": false,
|
||||
"~:is-blocked": false,
|
||||
"~:theme": "",
|
||||
"~:default-team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
|
||||
"~:default-project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
|
||||
"~:created-at": "~m1713533116365",
|
||||
"~:modified-at": "~m1713533116365",
|
||||
"~:props": {
|
||||
"~:nudge": {
|
||||
"~:big": 10,
|
||||
"~:small": 1
|
||||
},
|
||||
"~:v2-info-shown": true,
|
||||
"~:viewed-tutorial?": false,
|
||||
"~:viewed-walkthrough?": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -29,6 +29,42 @@ export class RegisterPage extends BasePage {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mocks a successful email-verification token exchange (the link the
|
||||
* user clicks from the verification email) and every RPC the dashboard
|
||||
* needs to render right after landing on it, so the flow can be
|
||||
* exercised end-to-end without a real backend.
|
||||
*/
|
||||
async setupEmailVerificationSuccess() {
|
||||
await this.mockConfigFlags(["disable-onboarding"]);
|
||||
await this.mockRPC(
|
||||
"verify-token",
|
||||
"register/verify-token-email-verified.json",
|
||||
);
|
||||
await this.mockRPCs({
|
||||
"get-teams": "logged-in-user/get-teams-default.json",
|
||||
"get-font-variants?team-id=*":
|
||||
"logged-in-user/get-font-variants-empty.json",
|
||||
"get-projects?team-id=*": "logged-in-user/get-projects-default.json",
|
||||
"get-team-members?team-id=*":
|
||||
"logged-in-user/get-team-members-your-penpot.json",
|
||||
"get-team-users?team-id=*":
|
||||
"logged-in-user/get-team-users-single-user.json",
|
||||
"get-unread-comment-threads?team-id=*":
|
||||
"logged-in-user/get-team-users-single-user.json",
|
||||
"get-team-recent-files?team-id=*":
|
||||
"logged-in-user/get-team-recent-files-empty.json",
|
||||
"get-profiles-for-file-comments":
|
||||
"logged-in-user/get-profiles-for-file-comments-empty.json",
|
||||
"get-builtin-templates":
|
||||
"logged-in-user/get-built-in-templates-empty.json",
|
||||
});
|
||||
}
|
||||
|
||||
async goToVerifyToken(token = "verify-email-token") {
|
||||
await this.page.goto(`/#/auth/verify-token?token=${token}`);
|
||||
}
|
||||
|
||||
static async init(page) {
|
||||
await BasePage.init(page);
|
||||
}
|
||||
|
||||
45
frontend/playwright/ui/specs/email-verification.spec.js
Normal file
45
frontend/playwright/ui/specs/email-verification.spec.js
Normal file
@ -0,0 +1,45 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { RegisterPage } from "../pages/RegisterPage";
|
||||
|
||||
// Regression test for the bug where a freshly verified account (whose
|
||||
// profile never had a theme persisted) ended up with an empty string as
|
||||
// its theme instead of falling back to the dark default: the workspace
|
||||
// switched to light mode and Settings > UI Theme showed a blank field.
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await RegisterPage.initWithLoggedOutUser(page);
|
||||
});
|
||||
|
||||
test.describe("Email verification", () => {
|
||||
test("Newly verified account defaults to the dark theme", async ({
|
||||
page,
|
||||
}) => {
|
||||
const registerPage = new RegisterPage(page);
|
||||
await registerPage.setupEmailVerificationSuccess();
|
||||
|
||||
await registerPage.goToVerifyToken();
|
||||
await page.waitForURL("**/dashboard/**");
|
||||
|
||||
// `default` is the body class applied for dark theme, `light` for
|
||||
// light theme (see app.util.theme/set-color-scheme).
|
||||
await expect(page.locator("body")).toHaveClass(/default/);
|
||||
await expect(page.locator("body")).not.toHaveClass(/light/);
|
||||
});
|
||||
|
||||
test("Settings > UI Theme shows Penpot Dark (default) selected, not blank", async ({
|
||||
page,
|
||||
}) => {
|
||||
const registerPage = new RegisterPage(page);
|
||||
await registerPage.setupEmailVerificationSuccess();
|
||||
|
||||
await registerPage.goToVerifyToken();
|
||||
await page.waitForURL("**/dashboard/**");
|
||||
|
||||
await page.goto("/#/settings/options");
|
||||
|
||||
// The language select is the first combobox on the page, the theme
|
||||
// select is the second one.
|
||||
const themeSelect = page.getByRole("combobox").nth(1);
|
||||
await expect(themeSelect).toHaveText("Penpot Dark (default)");
|
||||
});
|
||||
});
|
||||
@ -43,28 +43,30 @@
|
||||
(defn set-profile
|
||||
"Initialize profile state, only logged-in profile data should be
|
||||
passed to this event"
|
||||
[{:keys [id] :as profile}]
|
||||
(ptk/reify ::set-profile
|
||||
IDeref
|
||||
(-deref [_] profile)
|
||||
[profile]
|
||||
(let [profile (update profile :theme not-empty)
|
||||
id (:id profile)]
|
||||
(ptk/reify ::set-profile
|
||||
IDeref
|
||||
(-deref [_] profile)
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc :profile-id id)
|
||||
(assoc :profile profile)))
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc :profile-id id)
|
||||
(assoc :profile profile)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [profile (:profile state)]
|
||||
(->> (rx/from (i18n/set-locale (:lang profile)))
|
||||
(rx/ignore))))
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [profile (:profile state)]
|
||||
(->> (rx/from (i18n/set-locale (:lang profile)))
|
||||
(rx/ignore))))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(let [profile (:profile state)]
|
||||
(swap! storage/user assoc :profile profile)
|
||||
(plugins.register/init)))))
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(let [profile (:profile state)]
|
||||
(swap! storage/user assoc :profile profile)
|
||||
(plugins.register/init))))))
|
||||
|
||||
(def profile-fetched?
|
||||
(ptk/type? ::profile-fetched))
|
||||
|
||||
@ -204,20 +204,27 @@
|
||||
|
||||
(defn- bind!
|
||||
[shortcuts]
|
||||
(->> shortcuts
|
||||
(remove #(:disabled (second %)))
|
||||
(run! (fn [[key {:keys [command fn type overwrite]}]]
|
||||
(let [callback (wrap-cb key fn)
|
||||
commands (if (vector? command)
|
||||
(into-array command)
|
||||
#js [command])]
|
||||
(if (vector? type)
|
||||
(do (mousetrap/bind commands callback (nth type 0) overwrite)
|
||||
(mousetrap/bind commands callback (nth type 1) overwrite))
|
||||
(let [undefined (js* "(void 0)")]
|
||||
(if type
|
||||
(mousetrap/bind commands callback type overwrite)
|
||||
(mousetrap/bind commands callback undefined overwrite)))))))))
|
||||
(let [entries (remove #(:disabled (second %)) shortcuts)
|
||||
bind-fn (fn [[key {:keys [command fn type overwrite]}]]
|
||||
(let [callback (wrap-cb key fn)
|
||||
commands (if (vector? command)
|
||||
(into-array command)
|
||||
#js [command])]
|
||||
(if (vector? type)
|
||||
(do (mousetrap/bind commands callback (nth type 0) overwrite)
|
||||
(mousetrap/bind commands callback (nth type 1) overwrite))
|
||||
(let [undefined (js* "(void 0)")]
|
||||
(if type
|
||||
(mousetrap/bind commands callback type overwrite)
|
||||
(mousetrap/bind commands callback undefined overwrite))))))]
|
||||
;; Bind non-overwrite entries first so that entries flagged with
|
||||
;; `:overwrite` are bound last and can reliably splice out the
|
||||
;; colliding callbacks bound earlier (mousetrap's overwrite only
|
||||
;; removes callbacks that were already registered for the same
|
||||
;; combo). Map iteration order is hash-based, so we must force the
|
||||
;; order explicitly.
|
||||
(run! bind-fn (remove (comp :overwrite second) entries))
|
||||
(run! bind-fn (filter (comp :overwrite second) entries))))
|
||||
|
||||
(defn- reset!
|
||||
([]
|
||||
|
||||
@ -37,6 +37,7 @@
|
||||
:command "p"
|
||||
:subsections [:path-editor]
|
||||
:section [:workspace]
|
||||
:overwrite true
|
||||
:fn #(st/emit! (drp/change-edit-mode :draw))}
|
||||
|
||||
:add-node {:tooltip (ds/shift "+")
|
||||
@ -49,7 +50,9 @@
|
||||
:command ["del" "backspace"]
|
||||
:subsections [:path-editor]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (drp/remove-node))}
|
||||
:overwrite true
|
||||
:fn #(st/emit!
|
||||
(drp/remove-node))}
|
||||
|
||||
:merge-nodes {:tooltip (ds/meta "J")
|
||||
:command (ds/c-mod "j")
|
||||
@ -67,6 +70,7 @@
|
||||
:command "k"
|
||||
:subsections [:path-editor]
|
||||
:section [:workspace]
|
||||
:overwrite true
|
||||
:fn #(st/emit! (drp/separate-nodes))}
|
||||
|
||||
:make-corner {:tooltip "X"
|
||||
@ -79,6 +83,7 @@
|
||||
:command "c"
|
||||
:subsections [:path-editor]
|
||||
:section [:workspace]
|
||||
:overwrite true
|
||||
:fn #(st/emit! (drp/make-curve))}
|
||||
|
||||
:snap-nodes {:tooltip (ds/meta "'")
|
||||
@ -89,10 +94,22 @@
|
||||
:fn #(st/emit! (drp/toggle-snap))}
|
||||
|
||||
:escape {:tooltip (ds/esc)
|
||||
:command ["escape" "enter" "v"]
|
||||
:command ["escape" "v"]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (esc-pressed))}
|
||||
|
||||
;; Reuses the `:start-editing` key (instead of adding "enter" to
|
||||
;; the `:escape` command above) so that merging this shortcut set
|
||||
;; on top of the base workspace shortcuts (see `dsc/push-shortcuts`)
|
||||
;; deterministically replaces the workspace's `enter` binding
|
||||
;; (which enters path edit mode) instead of both ending up bound
|
||||
;; to the same physical key at once.
|
||||
:start-editing {:tooltip (ds/enter)
|
||||
:command "enter"
|
||||
:section [:workspace]
|
||||
:overwrite true
|
||||
:fn #(st/emit! (esc-pressed))}
|
||||
|
||||
:undo {:tooltip (ds/meta "Z")
|
||||
:command (ds/c-mod "z")
|
||||
:section [:workspace]
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
(ns app.main.ui.routes
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uri :as u]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
@ -21,6 +22,12 @@
|
||||
[cuerdas.core :as str]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(def ^:private sso-authorization-max-age-ms
|
||||
(* 5 60 1000))
|
||||
|
||||
(defonce ^:private sso-authorization-cache
|
||||
(atom {}))
|
||||
|
||||
(def routes
|
||||
[["/auth"
|
||||
["/login" :auth-login]
|
||||
@ -102,26 +109,43 @@
|
||||
"Authorization filter for dashboard and workspace routes.
|
||||
Checks if the team being navigated to has an organization with SSO
|
||||
active. If so, calls :check-nitrate-sso and either proceeds with navigation
|
||||
or redirects to the SSO provider URL."
|
||||
or redirects to the SSO provider URL. Successful checks are cached for five
|
||||
minutes per profile and team; redirect results are never cached."
|
||||
[match send-event-info? url]
|
||||
(let [route-name (name (get-in match [:data :name]))
|
||||
relevant? (and (contains? cf/flags :admin-console)
|
||||
(or (str/starts-with? route-name "dashboard")
|
||||
(str/starts-with? route-name "workspace")))
|
||||
team-id-str (when relevant?
|
||||
(or (get-in match [:query-params :team-id])
|
||||
(get-in match [:params :path :team-id])))
|
||||
team-id (some-> team-id-str uuid/parse*)]
|
||||
(if (some? team-id)
|
||||
(let [route-name (name (get-in match [:data :name]))
|
||||
relevant? (and (contains? cf/flags :admin-console)
|
||||
(or (str/starts-with? route-name "dashboard")
|
||||
(str/starts-with? route-name "workspace")))
|
||||
team-id-str (when relevant?
|
||||
(or (get-in match [:query-params :team-id])
|
||||
(get-in match [:params :path :team-id])))
|
||||
team-id (some-> team-id-str uuid/parse*)
|
||||
profile-id (get-in @st/state [:profile :id])
|
||||
cache-key [profile-id team-id]
|
||||
authorized-at (get @sso-authorization-cache cache-key)
|
||||
cache-valid? (and (some? authorized-at)
|
||||
(< (ct/diff-ms authorized-at (ct/now))
|
||||
sso-authorization-max-age-ms))
|
||||
navigate #(st/emit! (rt/navigated match send-event-info?))]
|
||||
(cond
|
||||
(nil? team-id)
|
||||
(navigate)
|
||||
|
||||
cache-valid?
|
||||
(navigate)
|
||||
|
||||
:else
|
||||
(->> (rp/cmd! :check-nitrate-sso {:team-id team-id :url url})
|
||||
(rx/subs!
|
||||
(fn [{:keys [authorized redirect-uri]}]
|
||||
(if authorized
|
||||
(st/emit! (rt/navigated match send-event-info?))
|
||||
(when redirect-uri (st/emit! (rt/nav-raw :uri (str redirect-uri))))))
|
||||
(do
|
||||
(swap! sso-authorization-cache assoc cache-key (ct/now))
|
||||
(navigate))
|
||||
(when redirect-uri
|
||||
(st/emit! (rt/nav-raw :uri (str redirect-uri))))))
|
||||
(fn [cause]
|
||||
(errors/on-error cause))))
|
||||
(st/emit! (rt/navigated match send-event-info?)))))
|
||||
(errors/on-error cause)))))))
|
||||
|
||||
(defn- handle-sso-error-and-navigate
|
||||
"Check if the current route has an SSO error marker. If so, assign an
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
|
||||
.section-title,
|
||||
.subsection-title {
|
||||
@include t.use-typography("title-small");
|
||||
@include t.use-typography("headline-small");
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -43,6 +43,7 @@
|
||||
}
|
||||
|
||||
.subsection-title {
|
||||
block-size: $sz-32;
|
||||
text-transform: none;
|
||||
padding-inline-start: var(--sp-m);
|
||||
}
|
||||
|
||||
@ -0,0 +1,78 @@
|
||||
;; 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 INC Sucursal en España SL
|
||||
|
||||
(ns frontend-tests.data.workspace-path-edition-test
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.test-helpers.files :as cthf]
|
||||
[app.common.test-helpers.shapes :as cths]
|
||||
[app.main.data.shortcuts :as dsc]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.path.shortcuts :as psc]
|
||||
[app.main.data.workspace.selection :as dws]
|
||||
[app.main.data.workspace.shortcuts :as wsc]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.pages :as thp]
|
||||
[frontend-tests.helpers.state :as ths]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(t/use-fixtures :each
|
||||
{:before thp/reset-idmap!})
|
||||
|
||||
(defn- enter-command?
|
||||
[command]
|
||||
(if (vector? command)
|
||||
(some #(= % "enter") command)
|
||||
(= command "enter")))
|
||||
|
||||
(t/deftest test-enter-key-is-bound-once-while-path-editing
|
||||
;; Regression test for the physical "enter" key ending up bound to
|
||||
;; two different shortcuts at once while path editing is active: one
|
||||
;; that (re)enters edition mode and one that exits it. `push-shortcuts`
|
||||
;; merges shortcut groups by map key (see `app.main.data.shortcuts`),
|
||||
;; not by physical key/command, so two shortcuts under different keys
|
||||
;; that both claim "enter" survive the merge and both would fire on a
|
||||
;; single keypress, breaking the toggle.
|
||||
(let [file (cthf/sample-file :file1)
|
||||
store (ths/setup-store file)]
|
||||
(ptk/emit! store (dsc/push-shortcuts ::workspace wsc/shortcuts :workspace))
|
||||
(ptk/emit! store (dsc/push-shortcuts ::path psc/shortcuts :workspace :merge-shortcuts :auto))
|
||||
(let [effective (get-in @store [:shortcuts ::path])
|
||||
matches (->> effective
|
||||
(filter (fn [[_ sc]] (enter-command? (:command sc))))
|
||||
(map first))]
|
||||
(t/is (= 1 (count matches))
|
||||
(str "expected exactly one shortcut bound to \"enter\" while path editing, got " matches)))))
|
||||
|
||||
(defn- run-scenario
|
||||
[shape-type]
|
||||
(let [file (-> (cthf/sample-file :file1)
|
||||
(cths/add-sample-shape :test-shape :type shape-type))
|
||||
shape-id (:id (cths/get-shape file :test-shape))
|
||||
store (ths/setup-store file)]
|
||||
;; Select the shape, then reproduce what a physical Enter keypress
|
||||
;; now dispatches at each step: `start-editing-selected` to enter
|
||||
;; path edition mode, `esc-pressed` (-> :interrupt) to exit it, and
|
||||
;; `start-editing-selected` again to re-enter.
|
||||
(ptk/emit! store (dws/select-shapes (d/ordered-set shape-id)))
|
||||
|
||||
(ptk/emit! store (dw/start-editing-selected))
|
||||
(t/is (= shape-id (get-in @store [:workspace-local :edition]))
|
||||
(str "expected " (name shape-type) " to enter path edition mode"))
|
||||
|
||||
(ptk/emit! store (psc/esc-pressed))
|
||||
(t/is (nil? (get-in @store [:workspace-local :edition]))
|
||||
(str "expected " (name shape-type) " to exit path edition mode"))
|
||||
(t/is (= #{shape-id} (get-in @store [:workspace-local :selected]))
|
||||
(str "expected " (name shape-type) " to remain selected after exiting path edition mode"))
|
||||
|
||||
(ptk/emit! store (dw/start-editing-selected))
|
||||
(t/is (= shape-id (get-in @store [:workspace-local :edition]))
|
||||
(str "expected " (name shape-type) " to enter path edition mode again"))))
|
||||
|
||||
(t/deftest test-enter-toggles-path-editing-mode
|
||||
(doseq [shape-type [:rect :circle :path :image]]
|
||||
(run-scenario shape-type)))
|
||||
@ -21,6 +21,7 @@
|
||||
[frontend-tests.data.workspace-mcp-test]
|
||||
[frontend-tests.data.workspace-media-test]
|
||||
[frontend-tests.data.workspace-pages-test]
|
||||
[frontend-tests.data.workspace-path-edition-test]
|
||||
[frontend-tests.data.workspace-reflow-test]
|
||||
[frontend-tests.data.workspace-shortcuts-test]
|
||||
[frontend-tests.data.workspace-texts-test]
|
||||
@ -72,6 +73,7 @@
|
||||
[frontend-tests.ui.gradient-handlers-test]
|
||||
[frontend-tests.ui.layout-container-multiple-test]
|
||||
[frontend-tests.ui.measures-menu-props-test]
|
||||
[frontend-tests.ui.routes-test]
|
||||
[frontend-tests.ui.settings-password-schema-test]
|
||||
[frontend-tests.ui.settings-shortcuts-test]
|
||||
[frontend-tests.util-clipboard-test]
|
||||
@ -114,6 +116,7 @@
|
||||
'frontend-tests.data.workspace-mcp-test
|
||||
'frontend-tests.data.workspace-media-test
|
||||
'frontend-tests.data.workspace-pages-test
|
||||
'frontend-tests.data.workspace-path-edition-test
|
||||
'frontend-tests.data.workspace-reflow-test
|
||||
'frontend-tests.data.workspace-shortcuts-test
|
||||
'frontend-tests.data.workspace-texts-test
|
||||
@ -163,6 +166,7 @@
|
||||
'frontend-tests.ui.gradient-handlers-test
|
||||
'frontend-tests.ui.layout-container-multiple-test
|
||||
'frontend-tests.ui.measures-menu-props-test
|
||||
'frontend-tests.ui.routes-test
|
||||
'frontend-tests.render-dimensions-test
|
||||
'frontend-tests.text-editor-paste-guard-test
|
||||
'frontend-tests.ui.settings-password-schema-test
|
||||
|
||||
85
frontend/test/frontend_tests/ui/routes_test.cljs
Normal file
85
frontend/test/frontend_tests/ui/routes_test.cljs
Normal file
@ -0,0 +1,85 @@
|
||||
;; 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 INC Sucursal en España SL
|
||||
|
||||
(ns frontend-tests.ui.routes-test
|
||||
(:require
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.routes :as routes]
|
||||
[beicon.v2.core :as rx]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.mock :as mock]))
|
||||
|
||||
(defn- workspace-match
|
||||
[team-id]
|
||||
{:data {:name :workspace}
|
||||
:params {:path {}}
|
||||
:query-params {:team-id (str team-id)}})
|
||||
|
||||
(t/deftest sso-check-is-cached-for-five-minutes
|
||||
(let [team-id (uuid/next)
|
||||
match (workspace-match team-id)
|
||||
now (atom (ct/inst "2026-08-11T10:00:00Z"))
|
||||
rpc-calls (atom 0)
|
||||
events (atom [])]
|
||||
(with-redefs [cf/flags (conj cf/flags :admin-console)
|
||||
ct/now (mock/stub (fn [] @now))
|
||||
rp/cmd! (mock/stub
|
||||
(fn [command params]
|
||||
(t/is (= :check-nitrate-sso command))
|
||||
(t/is (= team-id (:team-id params)))
|
||||
(swap! rpc-calls inc)
|
||||
(rx/of {:authorized true})))
|
||||
st/emit! (mock/stub
|
||||
(fn [& emitted]
|
||||
(swap! events into emitted)))]
|
||||
(#'routes/check-sso-and-navigate match true "https://penpot.example.com/#/workspace")
|
||||
(reset! now (ct/plus @now #js {:minutes 4 :seconds 59}))
|
||||
(#'routes/check-sso-and-navigate match true "https://penpot.example.com/#/workspace")
|
||||
|
||||
(t/is (= 1 @rpc-calls))
|
||||
(t/is (= 2 (count @events))))))
|
||||
|
||||
(t/deftest sso-check-is-refreshed-after-five-minutes
|
||||
(let [team-id (uuid/next)
|
||||
match (workspace-match team-id)
|
||||
now (atom (ct/inst "2026-08-11T10:00:00Z"))
|
||||
rpc-calls (atom 0)]
|
||||
(with-redefs [cf/flags (conj cf/flags :admin-console)
|
||||
ct/now (mock/stub (fn [] @now))
|
||||
rp/cmd! (mock/stub
|
||||
(fn [_ _]
|
||||
(swap! rpc-calls inc)
|
||||
(rx/of {:authorized true})))
|
||||
st/emit! mock/noop]
|
||||
(#'routes/check-sso-and-navigate match true "https://penpot.example.com/#/workspace")
|
||||
(reset! now (ct/plus @now #js {:minutes 5}))
|
||||
(#'routes/check-sso-and-navigate match true "https://penpot.example.com/#/workspace")
|
||||
|
||||
(t/is (= 2 @rpc-calls)))))
|
||||
|
||||
(t/deftest sso-redirect-result-is-not-cached
|
||||
(let [team-id (uuid/next)
|
||||
match (workspace-match team-id)
|
||||
rpc-calls (atom 0)
|
||||
events (atom [])]
|
||||
(with-redefs [cf/flags (conj cf/flags :admin-console)
|
||||
rp/cmd! (mock/stub
|
||||
(fn [_ _]
|
||||
(swap! rpc-calls inc)
|
||||
(rx/of {:authorized false
|
||||
:redirect-uri "https://idp.example.com/authorize"})))
|
||||
st/emit! (mock/stub
|
||||
(fn [& emitted]
|
||||
(swap! events into emitted)))]
|
||||
(#'routes/check-sso-and-navigate match true "https://penpot.example.com/#/workspace")
|
||||
(#'routes/check-sso-and-navigate match true "https://penpot.example.com/#/workspace")
|
||||
|
||||
(t/is (= 2 @rpc-calls))
|
||||
(t/is (= 2 (count @events))))))
|
||||
@ -1243,7 +1243,7 @@ msgstr "Your project has been moved successfully"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.team-belong-organization"
|
||||
msgstr "This team now belongs to %s"
|
||||
msgstr "This team is now part of the organization %s"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1602
|
||||
msgid "dashboard.team-info"
|
||||
@ -1255,7 +1255,7 @@ msgstr "Team members"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.team-no-longer-belong-organization"
|
||||
msgstr "This team no longer belongs to the organization %s"
|
||||
msgstr "This team is no longer part of the organization %s"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1609
|
||||
msgid "dashboard.team-organization"
|
||||
|
||||
@ -1254,7 +1254,7 @@ msgstr "Tu proyecto ha sido movido con éxito"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.team-belong-organization"
|
||||
msgstr "Este equipo ahora pertenece a la organización %s"
|
||||
msgstr "Este equipo ahora es parte de la organización %s"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1602
|
||||
msgid "dashboard.team-info"
|
||||
@ -1266,7 +1266,7 @@ msgstr "Integrantes del equipo"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.team-no-longer-belong-organization"
|
||||
msgstr "Este equipo ya no pertenece a la organización %s"
|
||||
msgstr "Este equipo ya no es parte de la organización %s"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1609
|
||||
msgid "dashboard.team-organization"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user