mirror of
https://github.com/penpot/penpot.git
synced 2026-10-03 01:06:14 +00:00
The event was written twice per accepted organization invitation. The backend submitted it, and the browser then re-submitted a copy of the props that the backend had already put in the response under `:organization-invitation-audit` (`handle-token :team-invitation` in `verify-token.cljs`). Both rows carried the same name with different prop vocabularies, and the browser copy only existed when the browser finished the flow. Emit the event from the backend only. It now also carries the three props that lived in the browser copy: the organization member count before the add, the add source, and whether the invitee also joined a team. The origin moves to the event context as `:event-origin`. The response no longer includes `:organization-invitation-audit`, so the browser stops emitting the event and `verify-token.cljs` drops its `app.main.data.event` require. The `accept-*` events of this command now share one prop vocabulary: `:profile-id` for the accepting profile, `:invited-by` for the inviter and `:profile-email` for the email, replacing the mix of `:user-id`/`:user-who-send-invitation` and `:email`. Audit consumers of `accept-organization-invitation` now see one row per acceptance instead of two, and must read the new prop names. AI-assisted-by: space-bunny-free
200 lines
10 KiB
Clojure
200 lines
10 KiB
Clojure
;; 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
|
|
|
|
(ns frontend-tests.data.nitrate-test
|
|
(:require
|
|
[app.common.time :as ct]
|
|
[app.common.uri :as u]
|
|
[app.main.data.event :as ev]
|
|
[app.main.data.nitrate :as dnt]
|
|
[app.main.data.nitrate-audit :as nitrate-audit]
|
|
[cljs.test :as t :include-macros true]))
|
|
|
|
(t/deftest account-age-days-test
|
|
(with-redefs [ct/now (constantly (ct/inst "2026-07-27T12:00:00Z"))]
|
|
(t/testing "returns the number of complete days since account creation"
|
|
(t/is (= 10
|
|
(dnt/account-age-days
|
|
{:created-at (ct/inst "2026-07-17T00:00:00Z")}))))
|
|
|
|
(t/testing "does not return negative ages"
|
|
(t/is (= 0
|
|
(dnt/account-age-days
|
|
{:created-at (ct/inst "2026-07-28T00:00:00Z")}))))
|
|
|
|
(t/testing "returns nil when the creation date is not an instant"
|
|
(t/is (nil? (dnt/account-age-days {})))
|
|
(t/is (nil? (dnt/account-age-days {:created-at "invalid"}))))))
|
|
|
|
(t/deftest add-team-to-organization-audit-event-test
|
|
(with-redefs [ct/now (constantly (ct/inst "2026-07-27T12:00:00Z"))]
|
|
(t/testing "reports creating the first team in an organization"
|
|
(let [event @(nitrate-audit/add-team-to-organization-event
|
|
{:organization-id "organization-1"
|
|
:organization-team-count-before 0
|
|
:team-previous-organization-status "no-organization"
|
|
:add-method "create-team-in-organization"
|
|
:subscription-status "trialing"})]
|
|
(t/is (= "add-team-to-organization" (::ev/name event)))
|
|
(t/is (= "dashboard:create-team-in-organization" (::ev/origin event)))
|
|
(t/is (= {:is-your-penpot false
|
|
:add-method "create-team-in-organization"
|
|
:organization-id "organization-1"
|
|
:organization-team-count-before 0
|
|
:team-previous-organization-status "no-organization"
|
|
:is-first-team-in-organization true
|
|
:subscription-status "trialing"}
|
|
(dissoc event ::ev/name ::ev/origin)))))
|
|
|
|
(t/testing "reports moving an older team from another organization"
|
|
(let [event @(nitrate-audit/add-team-to-organization-event
|
|
{:team {:id "team-2"
|
|
:created-at (ct/inst "2026-07-17T12:00:00Z")}
|
|
:organization-id "organization-2"
|
|
:organization-team-count-before 3
|
|
:team-previous-organization-status "other-organization"
|
|
:add-method "move-existing-team-to-organization"
|
|
:subscription-status "active"})]
|
|
(t/is (= "dashboard:move-team-to-organization" (::ev/origin event)))
|
|
(t/is (= 10 (:team-age-days event)))
|
|
(t/is (false? (:is-first-team-in-organization event)))))))
|
|
|
|
(t/deftest organization-team-count-test
|
|
(let [organization-id "organization-1"
|
|
teams [{:id "default"
|
|
:is-default true
|
|
:organization-id organization-id}
|
|
{:id "team-1"
|
|
:is-default false
|
|
:organization-id organization-id}
|
|
{:id "team-2"
|
|
:is-default false
|
|
:organization {:id organization-id}}
|
|
{:id "other-team"
|
|
:is-default false
|
|
:organization-id "organization-2"}]]
|
|
(t/is (= 2
|
|
(nitrate-audit/organization-team-count
|
|
teams
|
|
organization-id)))))
|
|
|
|
(t/deftest delete-organization-member-audit-event-test
|
|
(with-redefs [ct/now (constantly (ct/inst "2026-07-27T12:00:00Z"))]
|
|
(let [event @(nitrate-audit/delete-organization-member-event
|
|
{:organization-id "organization-1"
|
|
:user-id "profile-1"
|
|
:user-who-delete-member "profile-1"
|
|
:deleted-by-role "organization-member"
|
|
:member-added-at (ct/inst "2026-07-17T12:00:00Z")
|
|
:organization-member-count-before 4
|
|
:subscription-status "trial"})]
|
|
(t/is (= "delete-organization-member" (::ev/name event)))
|
|
(t/is (nil? (::ev/origin event)))
|
|
(t/is (= {:organization-id "organization-1"
|
|
:user-id "profile-1"
|
|
:user-who-delete-member "profile-1"
|
|
:deleted-by-role "organization-member"
|
|
:days-since-member-added 10
|
|
:organization-member-count-before 4
|
|
:subscription-status "trial"}
|
|
(dissoc event ::ev/name))))
|
|
|
|
(t/testing "keeps a null age when the membership date is unavailable"
|
|
(let [event @(nitrate-audit/delete-organization-member-event
|
|
{:organization-id "organization-1"
|
|
:user-id "profile-1"
|
|
:user-who-delete-member "profile-1"
|
|
:deleted-by-role "organization-member"
|
|
:organization-member-count-before 4
|
|
:subscription-status "active"})]
|
|
(t/is (contains? event :days-since-member-added))
|
|
(t/is (nil? (:days-since-member-added event)))))))
|
|
|
|
(t/deftest build-admin-console-url-preserves-public-uri-subpath
|
|
(t/testing "builds admin console routes below the configured Penpot subpath"
|
|
(let [public-uri (u/uri "https://example.com/penpot/")]
|
|
(t/is (= "https://example.com/penpot/admin-console/"
|
|
(dnt/build-admin-console-url public-uri "" nil)))
|
|
(t/is (= "https://example.com/penpot/admin-console/organization/my-organization/organization-id/people/"
|
|
(dnt/build-admin-console-url
|
|
public-uri
|
|
"organization/my-organization/organization-id/people/"
|
|
nil)))
|
|
(t/is (= {:action "create-organization"
|
|
:origin "dashboard:organization-switcher"}
|
|
(-> (dnt/build-admin-console-url
|
|
public-uri
|
|
""
|
|
{:action "create-organization"
|
|
:origin "dashboard:organization-switcher"})
|
|
u/uri
|
|
:query
|
|
u/query-string->map))))))
|
|
|
|
(t/deftest build-nitrate-callback-urls-preserves-hash-query
|
|
(t/testing "appends subscription to an existing query inside the hash route"
|
|
(let [base-url "https://localhost:3449/#/dashboard/recent?team-id=e6666530-0216-81c8-8007-f17d6087b74f"
|
|
callbacks (dnt/build-nitrate-callback-urls base-url base-url)]
|
|
(t/is (= "https://localhost:3449/#/dashboard/recent?team-id=e6666530-0216-81c8-8007-f17d6087b74f&subscription=subscribed-to-penpot-nitrate"
|
|
(:success-callback callbacks)))
|
|
(t/is (= "https://localhost:3449/#/dashboard/recent?team-id=e6666530-0216-81c8-8007-f17d6087b74f&subscription=nitrate-checkout-error"
|
|
(:error-callback callbacks)))
|
|
(t/is (= "https://localhost:3449/#/dashboard/recent?team-id=e6666530-0216-81c8-8007-f17d6087b74f&subscription=nitrate-checkout-finish-error"
|
|
(:finish-error-callback callbacks)))
|
|
(t/is (= "https://localhost:3449/#/dashboard/recent?team-id=e6666530-0216-81c8-8007-f17d6087b74f&subscription=nitrate-checkout-cancelled"
|
|
(:cancel-callback callbacks))))))
|
|
|
|
(t/deftest build-nitrate-callback-urls-adds-hash-query-when-missing
|
|
(t/testing "adds a hash query when the route has no query string yet"
|
|
(let [base-url "https://localhost:3449/#/settings/subscriptions"
|
|
callbacks (dnt/build-nitrate-callback-urls base-url base-url)]
|
|
(t/is (= "https://localhost:3449/#/settings/subscriptions?subscription=subscribed-to-penpot-nitrate"
|
|
(:success-callback callbacks))))))
|
|
|
|
(t/deftest build-nitrate-callback-urls-adds-regular-query-without-hash
|
|
(t/testing "falls back to the regular URL query when there is no hash route"
|
|
(let [base-url "https://localhost:3449/admin-console/licenses/billing?foo=bar"
|
|
callbacks (dnt/build-nitrate-callback-urls base-url base-url)]
|
|
(t/is (= "https://localhost:3449/admin-console/licenses/billing?foo=bar&subscription=subscribed-to-penpot-nitrate"
|
|
(:success-callback callbacks))))))
|
|
|
|
(t/deftest build-nitrate-callback-urls-accepts-uri-object
|
|
(t/testing "accepts a URI object as base url (used by the nitrate-form modal)"
|
|
(let [base-url (u/uri "https://localhost:3449/#/settings/subscriptions")
|
|
callbacks (dnt/build-nitrate-callback-urls base-url base-url)]
|
|
(t/is (= "https://localhost:3449/#/settings/subscriptions?subscription=nitrate-checkout-error"
|
|
(:error-callback callbacks))))))
|
|
|
|
(t/deftest build-nitrate-callback-urls-uses-separate-error-base
|
|
(t/testing "error callbacks use base-error-url while success/cancel use base-url"
|
|
(let [callbacks (dnt/build-nitrate-callback-urls
|
|
"https://localhost:3449/#/dashboard/recent"
|
|
"https://localhost:3449/#/settings/subscriptions")]
|
|
(t/is (= "https://localhost:3449/#/dashboard/recent?subscription=subscribed-to-penpot-nitrate"
|
|
(:success-callback callbacks)))
|
|
(t/is (= "https://localhost:3449/#/settings/subscriptions?subscription=nitrate-checkout-error"
|
|
(:error-callback callbacks)))
|
|
(t/is (= "https://localhost:3449/#/settings/subscriptions?subscription=nitrate-checkout-finish-error"
|
|
(:finish-error-callback callbacks)))
|
|
(t/is (= "https://localhost:3449/#/dashboard/recent?subscription=nitrate-checkout-cancelled"
|
|
(:cancel-callback callbacks))))))
|
|
|
|
(t/deftest go-to-subscription-url-is-a-string
|
|
(t/testing "must be a string so licenses/billing?callback=... survives query encoding"
|
|
(t/is (string? dnt/go-to-subscription-url))
|
|
(t/is (not (u/uri? dnt/go-to-subscription-url)))))
|
|
|
|
(t/deftest build-admin-console-billing-url-encodes-string-callback
|
|
(t/testing "billing callback query param round-trips as a real URL string"
|
|
(let [public-uri (u/uri "https://localhost:3449/")
|
|
callback "https://localhost:3449/#/settings/subscriptions"
|
|
href (dnt/build-admin-console-url
|
|
public-uri
|
|
"licenses/billing"
|
|
{:callback callback})
|
|
parsed (-> href u/uri :query u/query-string->map :callback)]
|
|
(t/is (= callback parsed)))))
|