mirror of
https://github.com/penpot/penpot.git
synced 2026-09-30 15:56:17 +00:00
* ✨ Add deployment info to events * ✨ Add get-environment-data RPC method Add a single public RPC method returning the deployment type and the enabled environment flags. It replaces get-deployment on the management API and get-enabled-flags on the main API. get-enabled-flags stays as a deprecated alias returning only the flags, so existing callers keep working until it is removed. The frontend event initialization now reads the flags from the new method. The exposed flags stay limited to audit-log and telemetry to avoid leaking internal backend flags. AI-assisted-by: deepseek-v4.1-flash --------- Co-authored-by: Andrey Antukh <niwi@niwi.nz>
596 lines
25 KiB
Clojure
596 lines
25 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 SUBSIDIARY SL
|
|
|
|
(ns backend-tests.rpc-audit-test
|
|
(:require
|
|
[app.common.pprint :as pp]
|
|
[app.common.time :as ct]
|
|
[app.common.uuid :as uuid]
|
|
[app.config :as cf]
|
|
[app.db :as db]
|
|
[app.loggers.audit :as audit]
|
|
[app.rpc :as-alias rpc]
|
|
[app.util.services :as sv]
|
|
[backend-tests.helpers :as th]
|
|
[clojure.test :as t]
|
|
[yetti.request]))
|
|
|
|
(t/use-fixtures :once th/state-init)
|
|
(t/use-fixtures :each th/database-reset)
|
|
|
|
(defn decode-row
|
|
[{:keys [props context] :as row}]
|
|
(cond-> row
|
|
(db/pgobject? props) (assoc :props (db/decode-transit-pgobject props))
|
|
(db/pgobject? context) (assoc :context (db/decode-transit-pgobject context))))
|
|
|
|
(def http-request
|
|
(reify
|
|
yetti.request/IRequest
|
|
(get-header [_ name]
|
|
(case name
|
|
"x-forwarded-for" "127.0.0.44"
|
|
"x-real-ip" "127.0.0.43"))))
|
|
|
|
(t/deftest prepare-context-initiator-is-plain-string
|
|
;; The initiator must always be a plain string, never a keyword: shared-key
|
|
;; authenticated callers (exporter, admin-console) arrive as keywords on
|
|
;; :app.http/auth-key-id and transit would persist them as "~:exporter".
|
|
(let [base {:headers {"x-forwarded-for" "127.0.0.44"}}]
|
|
(t/is (= "app" (:initiator (audit/prepare-context-from-request base))))
|
|
(t/is (= "exporter"
|
|
(:initiator (audit/prepare-context-from-request
|
|
(assoc base :app.http/auth-key-id :exporter)))))
|
|
(t/is (= "admin-console"
|
|
(:initiator (audit/prepare-context-from-request
|
|
(assoc base :app.http/auth-key-id :admin-console)))))
|
|
(t/is (string? (:initiator (audit/prepare-context-from-request
|
|
(assoc base :app.http/auth-key-id :nexus)))))))
|
|
|
|
(t/deftest push-events-1
|
|
(with-redefs [app.config/flags #{:audit-log}]
|
|
(let [prof (th/create-profile* 1 {:is-active true})
|
|
team-id (:default-team-id prof)
|
|
proj-id (:default-project-id prof)
|
|
|
|
params {::th/type :push-audit-events
|
|
::rpc/profile-id (:id prof)
|
|
:events [{:name "navigate"
|
|
:props {:project-id (str proj-id)
|
|
:team-id (str team-id)
|
|
:route "dashboard-files"}
|
|
:context {:engine "blink"}
|
|
:profile-id (:id prof)
|
|
:timestamp (ct/now)
|
|
:type "action"}]}
|
|
|
|
params (with-meta params
|
|
{:app.http/request http-request})
|
|
|
|
out (th/command! params)]
|
|
;; (th/print-result! out)
|
|
(t/is (nil? (:error out)))
|
|
(t/is (nil? (:result out)))
|
|
|
|
(let [[row :as rows] (->> (th/db-exec! ["select * from audit_log"])
|
|
(mapv decode-row))]
|
|
;; (pp/pprint rows)
|
|
(t/is (= 1 (count rows)))
|
|
(t/is (= (:id prof) (:profile-id row)))
|
|
(t/is (= "navigate" (:name row)))
|
|
(t/is (= "frontend" (:source row)))))))
|
|
|
|
(t/deftest push-events-2
|
|
(with-redefs [app.config/flags #{:audit-log}]
|
|
(let [prof (th/create-profile* 1 {:is-active true})
|
|
team-id (:default-team-id prof)
|
|
proj-id (:default-project-id prof)
|
|
|
|
params {::th/type :push-audit-events
|
|
::rpc/profile-id (:id prof)
|
|
:events [{:name "navigate"
|
|
:props {:project-id (str proj-id)
|
|
:team-id (str team-id)
|
|
:route "dashboard-files"}
|
|
:context {:engine "blink"}
|
|
:profile-id uuid/zero
|
|
:timestamp (ct/now)
|
|
:type "action"}]}
|
|
params (with-meta params
|
|
{:app.http/request http-request})
|
|
out (th/command! params)]
|
|
;; (th/print-result! out)
|
|
(t/is (nil? (:error out)))
|
|
(t/is (nil? (:result out)))
|
|
|
|
(let [[row :as rows] (->> (th/db-exec! ["select * from audit_log"])
|
|
(mapv decode-row))]
|
|
;; (pp/pprint rows)
|
|
(t/is (= 1 (count rows)))
|
|
(t/is (= (:id prof) (:profile-id row)))
|
|
(t/is (= "navigate" (:name row)))
|
|
(t/is (= "frontend" (:source row)))))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; TELEMETRY MODE (frontend ingest)
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(t/deftest push-events-telemetry-mode-stores-anonymized-row
|
|
;; When telemetry is enabled and audit-log is NOT, frontend events
|
|
;; must be stored with source="telemetry", empty props, zeroed ip,
|
|
;; and context filtered to safe keys only.
|
|
(with-redefs [cf/flags #{:telemetry}]
|
|
(let [prof (th/create-profile* 1 {:is-active true})
|
|
team-id (:default-team-id prof)
|
|
proj-id (:default-project-id prof)
|
|
|
|
params {::th/type :push-audit-events
|
|
::rpc/profile-id (:id prof)
|
|
:events [{:name "navigate"
|
|
:props {:project-id (str proj-id)
|
|
:team-id (str team-id)
|
|
:route "dashboard-files"}
|
|
:context {:browser "Chrome"
|
|
:browser-version "120.0"
|
|
:os "Linux"
|
|
:version "2.0.0"
|
|
:session "should-be-stripped"
|
|
:external-session-id "also-stripped"
|
|
:initiator "app"}
|
|
:timestamp (ct/now)
|
|
:type "action"}]}
|
|
|
|
params (with-meta params
|
|
{:app.http/request http-request})
|
|
out (th/command! params)]
|
|
|
|
(t/is (nil? (:error out)))
|
|
(t/is (nil? (:result out)))
|
|
|
|
(let [[row :as rows] (->> (th/db-exec! ["select * from audit_log"])
|
|
(mapv decode-row))]
|
|
(t/is (= 1 (count rows)))
|
|
;; source is telemetry:frontend, not frontend
|
|
(t/is (= "telemetry:frontend" (:source row)))
|
|
;; profile-id preserved
|
|
(t/is (= (:id prof) (:profile-id row)))
|
|
;; event name preserved
|
|
(t/is (= "navigate" (:name row)))
|
|
;; navigate events keep route and team-id; other keys stripped
|
|
(t/is (= {:route "dashboard-files"
|
|
:team-id (str team-id)}
|
|
(:props row)))
|
|
;; ip zeroed
|
|
(t/is (= "0.0.0.0" (str (:ip-addr row))))
|
|
;; timestamps truncated to day precision
|
|
(let [day-now (ct/truncate (ct/now) :days)]
|
|
(t/is (= day-now (:created-at row)))
|
|
(t/is (= day-now (:tracked-at row))))
|
|
;; context only contains safe keys
|
|
(let [ctx (:context row)]
|
|
(t/is (contains? ctx :browser))
|
|
(t/is (= "Chrome" (:browser ctx)))
|
|
(t/is (contains? ctx :os))
|
|
(t/is (= "Linux" (:os ctx)))
|
|
;; session-linking keys stripped
|
|
(t/is (not (contains? ctx :session)))
|
|
(t/is (not (contains? ctx :external-session-id))))))))
|
|
|
|
(t/deftest push-events-both-flags-creates-two-rows
|
|
;; When both :audit-log and :telemetry flags are active, two rows
|
|
;; should be stored: one full audit entry and one telemetry entry.
|
|
(with-redefs [cf/flags #{:audit-log :telemetry}]
|
|
(let [prof (th/create-profile* 1 {:is-active true})
|
|
params {::th/type :push-audit-events
|
|
::rpc/profile-id (:id prof)
|
|
:events [{:name "navigate"
|
|
:props {:route "dashboard"}
|
|
:context {:browser "Chrome"
|
|
:version "2.0.0"
|
|
:initiator "app"}
|
|
:timestamp (ct/now)
|
|
:type "action"}]}
|
|
params (with-meta params
|
|
{:app.http/request http-request})
|
|
out (th/command! params)]
|
|
|
|
(t/is (nil? (:error out)))
|
|
|
|
(let [[row1 row2 :as rows] (->> (th/db-exec! ["select * from audit_log order by source"])
|
|
(mapv decode-row))]
|
|
(t/is (= 2 (count rows)))
|
|
;; First row: full audit-log entry
|
|
(t/is (= "frontend" (:source row1)))
|
|
(t/is (contains? (:props row1) :route))
|
|
(t/is (not= "0.0.0.0" (str (:ip-addr row1))))
|
|
;; Second row: telemetry entry
|
|
(t/is (= "telemetry:frontend" (:source row2)))
|
|
(t/is (= "0.0.0.0" (str (:ip-addr row2))))
|
|
(let [day-now (ct/truncate (ct/now) :days)]
|
|
(t/is (= day-now (:created-at row2)))
|
|
(t/is (= day-now (:tracked-at row2))))))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; BACKEND PROCESS-EVENT PATH (RPC commands)
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(t/deftest backend-process-event-only-audit-log
|
|
(with-redefs [cf/flags #{:audit-log}]
|
|
(let [prof (th/create-profile* 1 {:is-active true})
|
|
event {:id (uuid/next)
|
|
:type "action"
|
|
:name "test-cmd"
|
|
:profile-id (:id prof)
|
|
:props {:full-key "full-val"}
|
|
:context {:version "2.0.0" :initiator "app"}
|
|
:tracked-at (ct/now)
|
|
:created-at (ct/now)
|
|
:source "backend"}]
|
|
(audit/submit* th/*system* event)
|
|
(let [[row :as rows] (->> (th/db-exec! ["select * from audit_log"])
|
|
(mapv decode-row))]
|
|
(t/is (= 1 (count rows)))
|
|
(t/is (= "backend" (:source row)))
|
|
(t/is (= "full-val" (get-in row [:props :full-key])))
|
|
(t/is (not= "0.0.0.0" (str (:ip-addr row))))))))
|
|
|
|
(t/deftest backend-process-event-only-telemetry
|
|
(with-redefs [cf/flags #{:telemetry}]
|
|
(let [prof (th/create-profile* 1 {:is-active true})
|
|
event {:id (uuid/next)
|
|
:type "action"
|
|
:name "test-cmd"
|
|
:profile-id (:id prof)
|
|
:props {:full-key "full-val"}
|
|
:context {:version "2.0.0" :initiator "app"}
|
|
:tracked-at (ct/now)
|
|
:created-at (ct/now)
|
|
:source "backend"}]
|
|
(audit/submit* th/*system* event)
|
|
(let [[row :as rows] (->> (th/db-exec! ["select * from audit_log"])
|
|
(mapv decode-row))]
|
|
(t/is (= 1 (count rows)))
|
|
(t/is (= "telemetry:backend" (:source row)))
|
|
(t/is (= "0.0.0.0" (str (:ip-addr row))))))))
|
|
|
|
(t/deftest backend-process-event-both-flags-creates-two-rows
|
|
;; When both :audit-log and :telemetry are active, the backend
|
|
;; process-event must store two rows: one full audit entry and one
|
|
;; telemetry entry.
|
|
(with-redefs [cf/flags #{:audit-log :telemetry}]
|
|
(let [prof (th/create-profile* 1 {:is-active true})
|
|
event {:id (uuid/next)
|
|
:type "action"
|
|
:name "test-cmd"
|
|
:profile-id (:id prof)
|
|
:props {:keep-me "important"}
|
|
:context {:version "2.0.0" :initiator "app"}
|
|
:tracked-at (ct/now)
|
|
:created-at (ct/now)
|
|
:source "backend"}]
|
|
(audit/submit* th/*system* event)
|
|
(let [[row1 row2 :as rows] (->> (th/db-exec! ["select * from audit_log order by source"])
|
|
(mapv decode-row))]
|
|
(t/is (= 2 (count rows)))
|
|
;; First row: full audit-log entry
|
|
(t/is (= "backend" (:source row1)))
|
|
(t/is (= "important" (get-in row1 [:props :keep-me])))
|
|
(t/is (not= "0.0.0.0" (str (:ip-addr row1))))
|
|
;; Second row: telemetry entry
|
|
(t/is (= "telemetry:backend" (:source row2)))
|
|
(t/is (= "0.0.0.0" (str (:ip-addr row2))))
|
|
(let [day-now (ct/truncate (ct/now) :days)]
|
|
(t/is (= day-now (:created-at row2)))
|
|
(t/is (= day-now (:tracked-at row2))))))))
|
|
|
|
(t/deftest push-events-disabled-when-no-flags-and-no-telemetry
|
|
;; When neither audit-log nor telemetry is enabled, no rows should
|
|
;; be stored.
|
|
(with-redefs [cf/flags #{}]
|
|
(let [prof (th/create-profile* 1 {:is-active true})
|
|
params {::th/type :push-audit-events
|
|
::rpc/profile-id (:id prof)
|
|
:events [{:name "navigate"
|
|
:props {:route "dashboard"}
|
|
:timestamp (ct/now)
|
|
:type "action"}]}
|
|
params (with-meta params
|
|
{:app.http/request http-request})
|
|
out (th/command! params)]
|
|
|
|
(t/is (nil? (:error out)))
|
|
(t/is (= 0 (count (th/db-exec! ["select * from audit_log"])))))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; GET-ENVIRONMENT-DATA
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(t/deftest get-environment-data-returns-selfhost-by-default
|
|
(with-redefs [cf/flags #{:audit-log}]
|
|
(let [out (th/command! {::th/type :get-environment-data})]
|
|
(t/is (th/success? out))
|
|
(t/is (= "selfhost" (-> out :result :deployment))))))
|
|
|
|
(t/deftest get-environment-data-returns-saas-when-configured
|
|
(with-redefs [cf/flags #{:telemetry}]
|
|
(binding [cf/config (assoc cf/config :is-saas true)]
|
|
(let [out (th/command! {::th/type :get-environment-data})]
|
|
(t/is (th/success? out))
|
|
(t/is (= "saas" (-> out :result :deployment)))))))
|
|
|
|
(t/deftest get-environment-data-only-exposes-event-flags
|
|
(with-redefs [cf/flags #{:audit-log :telemetry :graph :nrepl-server}]
|
|
(let [out (th/command! {::th/type :get-environment-data})]
|
|
(t/is (th/success? out))
|
|
(t/is (= #{:audit-log :telemetry} (-> out :result :flags))))))
|
|
|
|
(t/deftest get-environment-data-returns-empty-flags-when-none-enabled
|
|
(with-redefs [cf/flags #{:graph}]
|
|
(let [out (th/command! {::th/type :get-environment-data})]
|
|
(t/is (th/success? out))
|
|
(t/is (= #{} (-> out :result :flags))))))
|
|
|
|
(t/deftest get-enabled-flags-still-returns-flat-set
|
|
(with-redefs [cf/flags #{:audit-log :telemetry :graph}]
|
|
(let [out (th/command! {::th/type :get-enabled-flags})]
|
|
(t/is (th/success? out))
|
|
(t/is (= #{:audit-log :telemetry} (:result out))))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; PURE HELPER UNIT TESTS
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(t/deftest extract-utm-params-utm
|
|
;; UTM params are namespaced under :penpot
|
|
(let [result (audit/extract-utm-params {:utm_source "google"
|
|
:utm_medium "cpc"
|
|
:utm_campaign "spring"
|
|
:other "ignored"})]
|
|
(t/is (= "google" (:penpot/utm-source result)))
|
|
(t/is (= "cpc" (:penpot/utm-medium result)))
|
|
(t/is (= "spring" (:penpot/utm-campaign result)))
|
|
(t/is (not (contains? result :other)))))
|
|
|
|
(t/deftest extract-utm-params-mtm
|
|
;; MTM params are also namespaced under :penpot
|
|
(let [result (audit/extract-utm-params {:mtm_source "newsletter"
|
|
:mtm_medium "email"})]
|
|
(t/is (= "newsletter" (:penpot/mtm-source result)))
|
|
(t/is (= "email" (:penpot/mtm-medium result)))))
|
|
|
|
(t/deftest extract-utm-params-empty
|
|
(t/is (= {} (audit/extract-utm-params {})))
|
|
(t/is (= {} (audit/extract-utm-params {:foo "bar" :baz 42}))))
|
|
|
|
(t/deftest profile->props-selects-and-merges
|
|
;; Selects profile-props keys and merges with (:props profile)
|
|
(let [profile {:id (uuid/next)
|
|
:fullname "John"
|
|
:email "john@example.com"
|
|
:is-active true
|
|
:lang "en"
|
|
:deleted-field "gone"
|
|
:props {:custom-key "custom-val"
|
|
:newsletter-updates true}}
|
|
result (audit/profile->props profile)]
|
|
;; Selected keys from profile
|
|
(t/is (= "John" (:fullname result)))
|
|
(t/is (= "john@example.com" (:email result)))
|
|
(t/is (true? (:is-active result)))
|
|
(t/is (= "en" (:lang result)))
|
|
;; Merged from (:props profile)
|
|
(t/is (= "custom-val" (:custom-key result)))
|
|
(t/is (true? (:newsletter-updates result)))
|
|
;; Keys not in profile-props are excluded
|
|
(t/is (not (contains? result :deleted-field)))))
|
|
|
|
(t/deftest profile->props-removes-nils
|
|
(let [profile {:id (uuid/next) :fullname nil :email "a@b.com"}
|
|
result (audit/profile->props profile)]
|
|
(t/is (not (contains? result :fullname)))
|
|
(t/is (= "a@b.com" (:email result)))))
|
|
|
|
(t/deftest clean-props-removes-reserved
|
|
;; Reserved props (:session-id, :password, :old-password, :token) are stripped
|
|
(let [props {:name "test"
|
|
:session-id "sess-123"
|
|
:password "secret"
|
|
:old-password "old-secret"
|
|
:token "tok-456"
|
|
:valid-key "kept"}
|
|
result (audit/clean-props props)]
|
|
(t/is (= "test" (:name result)))
|
|
(t/is (= "kept" (:valid-key result)))
|
|
(t/is (not (contains? result :session-id)))
|
|
(t/is (not (contains? result :password)))
|
|
(t/is (not (contains? result :old-password)))
|
|
(t/is (not (contains? result :token)))))
|
|
|
|
(t/deftest clean-props-removes-qualified-keys
|
|
;; Qualified keywords (namespaced) are stripped
|
|
(let [props {:simple "kept"
|
|
::namespaced "stripped"
|
|
:app.rpc/also-stripped true}
|
|
result (audit/clean-props props)]
|
|
(t/is (= "kept" (:simple result)))
|
|
(t/is (not (contains? result ::namespaced)))
|
|
(t/is (not (contains? result :app.rpc/also-stripped)))))
|
|
|
|
(t/deftest clean-props-removes-nils
|
|
(let [props {:a nil :b "val" :c nil}
|
|
result (audit/clean-props props)]
|
|
(t/is (= "val" (:b result)))
|
|
(t/is (not (contains? result :a)))
|
|
(t/is (not (contains? result :c)))))
|
|
|
|
(t/deftest get-external-session-id-valid
|
|
(let [request (reify yetti.request/IRequest
|
|
(get-header [_ name]
|
|
(case name "x-external-session-id" "abc-123")))]
|
|
(t/is (= "abc-123" (audit/get-external-session-id request)))))
|
|
|
|
(t/deftest get-external-session-id-nil-when-missing
|
|
(let [request (reify yetti.request/IRequest
|
|
(get-header [_ _] nil))]
|
|
(t/is (nil? (audit/get-external-session-id request)))))
|
|
|
|
(t/deftest get-external-session-id-nil-when-null-string
|
|
(let [request (reify yetti.request/IRequest
|
|
(get-header [_ name]
|
|
(case name "x-external-session-id" "null")))]
|
|
(t/is (nil? (audit/get-external-session-id request)))))
|
|
|
|
(t/deftest get-external-session-id-nil-when-blank
|
|
(let [request (reify yetti.request/IRequest
|
|
(get-header [_ name]
|
|
(case name "x-external-session-id" " ")))]
|
|
(t/is (nil? (audit/get-external-session-id request)))))
|
|
|
|
(t/deftest get-external-session-id-nil-when-too-long
|
|
(let [long-id (apply str (repeat 300 "x"))
|
|
request (reify yetti.request/IRequest
|
|
(get-header [_ name]
|
|
(case name "x-external-session-id" long-id)))]
|
|
(t/is (nil? (audit/get-external-session-id request)))))
|
|
|
|
(t/deftest get-client-user-agent-valid
|
|
(let [request (reify yetti.request/IRequest
|
|
(get-header [_ name]
|
|
(case name "user-agent" "Mozilla/5.0 (Test)")))]
|
|
(t/is (= "Mozilla/5.0 (Test)" (audit/get-client-user-agent request)))))
|
|
|
|
(t/deftest get-client-user-agent-nil-when-missing
|
|
(let [request (reify yetti.request/IRequest
|
|
(get-header [_ _] nil))]
|
|
(t/is (nil? (audit/get-client-user-agent request)))))
|
|
|
|
(t/deftest get-client-user-agent-truncates-long
|
|
(let [long-ua (apply str (repeat 600 "x"))
|
|
request (reify yetti.request/IRequest
|
|
(get-header [_ name]
|
|
(case name "user-agent" long-ua)))]
|
|
(t/is (<= (count (audit/get-client-user-agent request)) 500))))
|
|
|
|
(t/deftest get-client-event-origin-valid
|
|
(let [get-client-event-origin (ns-resolve 'app.loggers.audit 'get-client-event-origin)
|
|
request (reify yetti.request/IRequest
|
|
(get-header [_ name]
|
|
(case name "x-event-origin" "workspace")))]
|
|
(t/is (= "workspace" (get-client-event-origin request)))))
|
|
|
|
(t/deftest get-client-event-origin-nil-when-null
|
|
(let [get-client-event-origin (ns-resolve 'app.loggers.audit 'get-client-event-origin)
|
|
request (reify yetti.request/IRequest
|
|
(get-header [_ name]
|
|
(case name "x-event-origin" "null")))]
|
|
(t/is (nil? (get-client-event-origin request)))))
|
|
|
|
(t/deftest get-client-event-origin-nil-when-blank
|
|
(let [get-client-event-origin (ns-resolve 'app.loggers.audit 'get-client-event-origin)
|
|
request (reify yetti.request/IRequest
|
|
(get-header [_ name]
|
|
(case name "x-event-origin" " ")))]
|
|
(t/is (nil? (get-client-event-origin request)))))
|
|
|
|
(t/deftest get-client-event-origin-truncates-long
|
|
(let [get-client-event-origin (ns-resolve 'app.loggers.audit 'get-client-event-origin)
|
|
long-origin (apply str (repeat 300 "a"))
|
|
request (reify yetti.request/IRequest
|
|
(get-header [_ name]
|
|
(case name "x-event-origin" long-origin)))]
|
|
(t/is (<= (count (get-client-event-origin request)) 200))))
|
|
|
|
(t/deftest get-client-version-valid
|
|
(let [get-client-version (ns-resolve 'app.loggers.audit 'get-client-version)
|
|
request (reify yetti.request/IRequest
|
|
(get-header [_ name]
|
|
(case name "x-frontend-version" "2.0.0")))]
|
|
(t/is (= "2.0.0" (get-client-version request)))))
|
|
|
|
(t/deftest get-client-version-nil-when-null
|
|
(let [get-client-version (ns-resolve 'app.loggers.audit 'get-client-version)
|
|
request (reify yetti.request/IRequest
|
|
(get-header [_ name]
|
|
(case name "x-frontend-version" "null")))]
|
|
(t/is (nil? (get-client-version request)))))
|
|
|
|
(t/deftest get-client-version-nil-when-blank
|
|
(let [get-client-version (ns-resolve 'app.loggers.audit 'get-client-version)
|
|
request (reify yetti.request/IRequest
|
|
(get-header [_ name]
|
|
(case name "x-frontend-version" " ")))]
|
|
(t/is (nil? (get-client-version request)))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; INSERT DEFAULTS
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(t/deftest insert-only-runs-with-audit-log-flag
|
|
;; insert must be a no-op when :audit-log flag is not set
|
|
(with-redefs [app.config/flags #{:telemetry}]
|
|
(audit/insert th/*system* {:name "test" :type "action"})
|
|
(t/is (= 0 (count (th/db-exec! ["select * from audit_log"]))))))
|
|
|
|
(t/deftest insert-sets-defaults
|
|
;; insert must set defaults and persist when :audit-log is set
|
|
(with-redefs [app.config/flags #{:audit-log}]
|
|
(audit/insert th/*system* {:name "test-action" :type "action"})
|
|
(let [[row] (->> (th/db-exec! ["select * from audit_log"])
|
|
(mapv decode-row))]
|
|
(t/is (some? row))
|
|
(t/is (= "test-action" (:name row)))
|
|
(t/is (= "action" (:type row)))
|
|
(t/is (= "backend" (:source row)))
|
|
(t/is (some? (:id row)))
|
|
(t/is (some? (:created-at row)))
|
|
(t/is (some? (:tracked-at row)))
|
|
(t/is (= {} (:props row)))
|
|
(t/is (= {} (:context row))))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; PREPARE-RPC-EVENT PROFILE-ID CONVERSION
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(t/deftest prepare-rpc-event-converts-string-profile-id-to-uuid
|
|
;; When result contains a string :profile-id (e.g. from error reports),
|
|
;; prepare-rpc-event must convert it to a UUID for audit schema compliance.
|
|
(let [prof (th/create-profile* 1 {:is-active true})
|
|
string-pid "33601240-a00b-11ea-ba1b-c554cc60e361"
|
|
expected #uuid "33601240-a00b-11ea-ba1b-c554cc60e361"
|
|
mdata {::sv/name "test-cmd"}
|
|
params {::rpc/profile-id (:id prof)
|
|
::rpc/request-id (uuid/next)
|
|
::rpc/request-at (ct/now)}
|
|
mock-req (reify
|
|
yetti.request/IRequest
|
|
(get-header [_ _] nil)
|
|
(remote-addr [_] "127.0.0.1"))
|
|
params (with-meta params {:app.http/request mock-req})
|
|
result {:profile-id string-pid :some-data "value"}
|
|
event (audit/prepare-rpc-event th/*system* mdata params result)]
|
|
;; profile-id must be a UUID, not a string
|
|
(t/is (uuid? (:profile-id event)))
|
|
(t/is (= expected (:profile-id event)))))
|
|
|
|
(t/deftest prepare-rpc-event-handles-invalid-string-profile-id
|
|
;; When result contains an invalid string :profile-id, it should fall back
|
|
;; to the RPC params profile-id (which is always a valid UUID).
|
|
(let [prof (th/create-profile* 1 {:is-active true})
|
|
mdata {::sv/name "test-cmd"}
|
|
params {::rpc/profile-id (:id prof)
|
|
::rpc/request-id (uuid/next)
|
|
::rpc/request-at (ct/now)}
|
|
mock-req (reify
|
|
yetti.request/IRequest
|
|
(get-header [_ _] nil)
|
|
(remote-addr [_] "127.0.0.1"))
|
|
params (with-meta params {:app.http/request mock-req})
|
|
result {:profile-id "not-a-valid-uuid"}
|
|
event (audit/prepare-rpc-event th/*system* mdata params result)]
|
|
;; profile-id must fall back to the RPC params profile-id
|
|
(t/is (uuid? (:profile-id event)))
|
|
(t/is (= (:id prof) (:profile-id event)))))
|