mirror of
https://github.com/penpot/penpot.git
synced 2026-09-24 12:56:15 +00:00
💥 Remove client-provided id from creation RPC commands (#11784)
The seven creation commands no longer accept an optional client id: create-file, create-project, create-team, create-team-with-invitations, upload-file-media-object, create-file-media-object-from-url and assemble-file-media-object. The server always generates the identifier; a sent id is ignored. Malli maps are open and the RPC layer never strips unknown params, so the handlers that would still honor an id (create-file, create-project) now drop it explicitly. Internal callers that pass remapped ids (project duplicate, binfile import) keep working. Closes #11783 AI-assisted-by: muse-spark-1.3-contributor
This commit is contained in:
parent
6498619f60
commit
2f679eaa0e
@ -7,6 +7,7 @@
|
|||||||
- RPC auth defaults to enabled. Public endpoints must set `::auth false` metadata explicitly.
|
- RPC auth defaults to enabled. Public endpoints must set `::auth false` metadata explicitly.
|
||||||
- The wrapper stack does auth before params validation, then auditing/rate/concurrency/metrics/retry/condition handling, with DB transaction handling inside that stack. `::db/transaction` metadata controls transaction wrapping.
|
- The wrapper stack does auth before params validation, then auditing/rate/concurrency/metrics/retry/condition handling, with DB transaction handling inside that stack. `::db/transaction` metadata controls transaction wrapping.
|
||||||
- Params with `::sm/params` are decoded/conformed through the JSON transformer and successful IObj results get `:encode/json` metadata. Legacy spec conforming only applies when no Malli params schema exists. Client params are stripped of qualified keys (`d/without-qualified`) before merging with the server auth context, so request bodies cannot override `::profile-id`, `::auth-type`, or `::token-perms`.
|
- Params with `::sm/params` are decoded/conformed through the JSON transformer and successful IObj results get `:encode/json` metadata. Legacy spec conforming only applies when no Malli params schema exists. Client params are stripped of qualified keys (`d/without-qualified`) before merging with the server auth context, so request bodies cannot override `::profile-id`, `::auth-type`, or `::token-perms`.
|
||||||
|
- Params schemas are open by default, so undeclared client keys reach the handler unless the map is `:closed true`. Creation commands (`create-file`, `create-project`, `create-team`, `create-team-with-invitations`, `upload-file-media-object`, `create-file-media-object-from-url`, `assemble-file-media-object`) use closed schemas: a client-provided `:id` fails with `:params-validation`. Their internal creation functions still accept an optional explicit `:id` for imports, duplicates and deterministic test fixtures.
|
||||||
- Nil RPC bodies become HTTP 204 unless explicit status metadata is present. Stream bodies default to `application/octet-stream` when no content type is set.
|
- Nil RPC bodies become HTTP 204 unless explicit status metadata is present. Stream bodies default to `application/octet-stream` when no content type is set.
|
||||||
|
|
||||||
## DB helpers
|
## DB helpers
|
||||||
|
|||||||
@ -72,15 +72,15 @@
|
|||||||
(bfc/get-file cfg (:id file)))))
|
(bfc/get-file cfg (:id file)))))
|
||||||
|
|
||||||
(def ^:private schema:create-file
|
(def ^:private schema:create-file
|
||||||
[:map {:title "create-file"}
|
[:map {:title "create-file" :closed true}
|
||||||
[:name [:string {:max 250}]]
|
[:name [:string {:max 250}]]
|
||||||
[:project-id ::sm/uuid]
|
[:project-id ::sm/uuid]
|
||||||
[:id {:optional true} ::sm/user-provided-uuid]
|
|
||||||
[:is-shared {:optional true} ::sm/boolean]
|
[:is-shared {:optional true} ::sm/boolean]
|
||||||
[:features {:optional true} ::cfeat/features]])
|
[:features {:optional true} ::cfeat/features]])
|
||||||
|
|
||||||
(sv/defmethod ::create-file
|
(sv/defmethod ::create-file
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
|
::doc/changes [["2.19" "The optional :id param is rejected with a params-validation error; the server always generates the identifier"]]
|
||||||
::doc/module :files
|
::doc/module :files
|
||||||
::webhooks/event? true
|
::webhooks/event? true
|
||||||
::sm/params schema:create-file
|
::sm/params schema:create-file
|
||||||
|
|||||||
@ -47,8 +47,7 @@
|
|||||||
WHERE f.id = ?")
|
WHERE f.id = ?")
|
||||||
|
|
||||||
(def ^:private schema:upload-file-media-object
|
(def ^:private schema:upload-file-media-object
|
||||||
[:map {:title "upload-file-media-object"}
|
[:map {:title "upload-file-media-object" :closed true}
|
||||||
[:id {:optional true} ::sm/user-provided-uuid]
|
|
||||||
[:file-id ::sm/uuid]
|
[:file-id ::sm/uuid]
|
||||||
[:is-local ::sm/boolean]
|
[:is-local ::sm/boolean]
|
||||||
[:name [:string {:max 250}]]
|
[:name [:string {:max 250}]]
|
||||||
@ -56,6 +55,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::upload-file-media-object
|
(sv/defmethod ::upload-file-media-object
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
|
::doc/changes [["2.19" "The optional :id param is rejected with a params-validation error; the server always generates the identifier"]]
|
||||||
::sm/params schema:upload-file-media-object
|
::sm/params schema:upload-file-media-object
|
||||||
::climit/id [[:process-image/by-profile ::rpc/profile-id]
|
::climit/id [[:process-image/by-profile ::rpc/profile-id]
|
||||||
[:process-image/global]]}
|
[:process-image/global]]}
|
||||||
@ -221,15 +221,15 @@
|
|||||||
(declare ^:private create-file-media-object-from-url)
|
(declare ^:private create-file-media-object-from-url)
|
||||||
|
|
||||||
(def ^:private schema:create-file-media-object-from-url
|
(def ^:private schema:create-file-media-object-from-url
|
||||||
[:map {:title "create-file-media-object-from-url"}
|
[:map {:title "create-file-media-object-from-url" :closed true}
|
||||||
[:file-id ::sm/uuid]
|
[:file-id ::sm/uuid]
|
||||||
[:is-local ::sm/boolean]
|
[:is-local ::sm/boolean]
|
||||||
[:url ::sm/uri]
|
[:url ::sm/uri]
|
||||||
[:id {:optional true} ::sm/user-provided-uuid]
|
|
||||||
[:name {:optional true} [:string {:max 250}]]])
|
[:name {:optional true} [:string {:max 250}]]])
|
||||||
|
|
||||||
(sv/defmethod ::create-file-media-object-from-url
|
(sv/defmethod ::create-file-media-object-from-url
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
|
::doc/changes [["2.19" "The optional :id param is rejected with a params-validation error; the server always generates the identifier"]]
|
||||||
::sm/params schema:create-file-media-object-from-url}
|
::sm/params schema:create-file-media-object-from-url}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||||
(files/check-edition-permissions! pool profile-id file-id)
|
(files/check-edition-permissions! pool profile-id file-id)
|
||||||
@ -550,21 +550,21 @@
|
|||||||
;; --- Chunked Upload: Assemble all chunks into a final media object
|
;; --- Chunked Upload: Assemble all chunks into a final media object
|
||||||
|
|
||||||
(def ^:private schema:assemble-file-media-object
|
(def ^:private schema:assemble-file-media-object
|
||||||
[:map {:title "assemble-file-media-object"}
|
[:map {:title "assemble-file-media-object" :closed true}
|
||||||
[:session-id ::sm/uuid]
|
[:session-id ::sm/uuid]
|
||||||
[:file-id ::sm/uuid]
|
[:file-id ::sm/uuid]
|
||||||
[:is-local ::sm/boolean]
|
[:is-local ::sm/boolean]
|
||||||
[:name [:string {:max 250}]]
|
[:name [:string {:max 250}]]
|
||||||
[:mtype :string]
|
[:mtype :string]])
|
||||||
[:id {:optional true} ::sm/user-provided-uuid]])
|
|
||||||
|
|
||||||
(sv/defmethod ::assemble-file-media-object
|
(sv/defmethod ::assemble-file-media-object
|
||||||
{::doc/added "2.17"
|
{::doc/added "2.17"
|
||||||
|
::doc/changes [["2.19" "The optional :id param is rejected with a params-validation error; the server always generates the identifier"]]
|
||||||
::sm/params schema:assemble-file-media-object
|
::sm/params schema:assemble-file-media-object
|
||||||
::climit/id [[:process-image/by-profile ::rpc/profile-id]
|
::climit/id [[:process-image/by-profile ::rpc/profile-id]
|
||||||
[:process-image/global]]}
|
[:process-image/global]]}
|
||||||
[{:keys [::db/pool] :as cfg}
|
[{:keys [::db/pool] :as cfg}
|
||||||
{:keys [::rpc/profile-id session-id file-id is-local name mtype id] :as params}]
|
{:keys [::rpc/profile-id session-id file-id is-local name mtype] :as params}]
|
||||||
(files/check-edition-permissions! pool profile-id file-id)
|
(files/check-edition-permissions! pool profile-id file-id)
|
||||||
|
|
||||||
(db/tx-run! cfg
|
(db/tx-run! cfg
|
||||||
@ -576,7 +576,6 @@
|
|||||||
(media.v/validate-media-type!)
|
(media.v/validate-media-type!)
|
||||||
(media.v/validate-media-size!))
|
(media.v/validate-media-size!))
|
||||||
mobj (create-file-media-object cfg (assoc params
|
mobj (create-file-media-object cfg (assoc params
|
||||||
:id id
|
|
||||||
:from-chunks? true
|
:from-chunks? true
|
||||||
:content content))]
|
:content content))]
|
||||||
|
|
||||||
|
|||||||
@ -196,13 +196,13 @@
|
|||||||
(assoc project :is-pinned false)))
|
(assoc project :is-pinned false)))
|
||||||
|
|
||||||
(def ^:private schema:create-project
|
(def ^:private schema:create-project
|
||||||
[:map {:title "create-project"}
|
[:map {:title "create-project" :closed true}
|
||||||
[:team-id ::sm/uuid]
|
[:team-id ::sm/uuid]
|
||||||
[:name [:string {:max 250 :min 1}]]
|
[:name [:string {:max 250 :min 1}]]])
|
||||||
[:id {:optional true} ::sm/user-provided-uuid]])
|
|
||||||
|
|
||||||
(sv/defmethod ::create-project
|
(sv/defmethod ::create-project
|
||||||
{::doc/added "1.18"
|
{::doc/added "1.18"
|
||||||
|
::doc/changes [["2.19" "The optional :id param is rejected with a params-validation error; the server always generates the identifier"]]
|
||||||
::webhooks/event? true
|
::webhooks/event? true
|
||||||
::sm/params schema:create-project}
|
::sm/params schema:create-project}
|
||||||
[cfg {:keys [::rpc/profile-id team-id] :as params}]
|
[cfg {:keys [::rpc/profile-id team-id] :as params}]
|
||||||
|
|||||||
@ -520,15 +520,15 @@
|
|||||||
(declare ^:private create-team-default-project)
|
(declare ^:private create-team-default-project)
|
||||||
|
|
||||||
(def ^:private schema:create-team
|
(def ^:private schema:create-team
|
||||||
[:map {:title "create-team"}
|
[:map {:title "create-team" :closed true}
|
||||||
[:name types.team/schema:team-name]
|
[:name types.team/schema:team-name]
|
||||||
[:features {:optional true} ::cfeat/features]
|
[:features {:optional true} ::cfeat/features]
|
||||||
[:id {:optional true} ::sm/user-provided-uuid]
|
|
||||||
[:organization-id {:optional true} ::sm/uuid]
|
[:organization-id {:optional true} ::sm/uuid]
|
||||||
[:is-default {:optional true} :boolean]])
|
[:is-default {:optional true} :boolean]])
|
||||||
|
|
||||||
(sv/defmethod ::create-team
|
(sv/defmethod ::create-team
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
|
::doc/changes [["2.19" "The optional :id param is rejected with a params-validation error; the server always generates the identifier"]]
|
||||||
::sm/params schema:create-team}
|
::sm/params schema:create-team}
|
||||||
[cfg {:keys [::rpc/profile-id organization-id] :as params}]
|
[cfg {:keys [::rpc/profile-id organization-id] :as params}]
|
||||||
|
|
||||||
@ -651,7 +651,7 @@
|
|||||||
(assoc team :default-project-id (:id project))))
|
(assoc team :default-project-id (:id project))))
|
||||||
|
|
||||||
(defn- create-team*
|
(defn- create-team*
|
||||||
[conn {:keys [id name is-default features] :as params}]
|
[conn {:keys [id name is-default features]}]
|
||||||
(let [id (or id (uuid/next))
|
(let [id (or id (uuid/next))
|
||||||
is-default (if (boolean? is-default) is-default false)
|
is-default (if (boolean? is-default) is-default false)
|
||||||
features (db/create-array conn "text" features)
|
features (db/create-array conn "text" features)
|
||||||
@ -690,6 +690,9 @@
|
|||||||
|
|
||||||
(defn create-project
|
(defn create-project
|
||||||
[conn {:keys [id team-id name is-default created-at modified-at]}]
|
[conn {:keys [id team-id name is-default created-at modified-at]}]
|
||||||
|
;; NOTE: the explicit id is kept for internal callers that duplicate or
|
||||||
|
;; import projects with a remapped id (see management.clj); the RPC
|
||||||
|
;; commands no longer accept a client-provided id.
|
||||||
(let [id (or id (uuid/next))
|
(let [id (or id (uuid/next))
|
||||||
is-default (if (boolean? is-default) is-default false)
|
is-default (if (boolean? is-default) is-default false)
|
||||||
name (d/normalize-string name)
|
name (d/normalize-string name)
|
||||||
|
|||||||
@ -535,15 +535,15 @@
|
|||||||
;; --- Mutation: Create Team & Invite Members
|
;; --- Mutation: Create Team & Invite Members
|
||||||
|
|
||||||
(def ^:private schema:create-team-with-invitations
|
(def ^:private schema:create-team-with-invitations
|
||||||
[:map {:title "create-team-with-invitations"}
|
[:map {:title "create-team-with-invitations" :closed true}
|
||||||
[:name [:string {:max 250}]]
|
[:name [:string {:max 250}]]
|
||||||
[:features {:optional true} ::cfeat/features]
|
[:features {:optional true} ::cfeat/features]
|
||||||
[:id {:optional true} ::sm/user-provided-uuid]
|
|
||||||
[:emails [::sm/set ::sm/email]]
|
[:emails [::sm/set ::sm/email]]
|
||||||
[:role types.team/schema:role]])
|
[:role types.team/schema:role]])
|
||||||
|
|
||||||
(sv/defmethod ::create-team-with-invitations
|
(sv/defmethod ::create-team-with-invitations
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
|
::doc/changes [["2.19" "The optional :id param is rejected with a params-validation error; the server always generates the identifier"]]
|
||||||
::doc/module :teams
|
::doc/module :teams
|
||||||
::sm/params schema:create-team-with-invitations
|
::sm/params schema:create-team-with-invitations
|
||||||
::db/transaction true}
|
::db/transaction true}
|
||||||
|
|||||||
@ -46,25 +46,23 @@
|
|||||||
(let [prof (th/create-profile* 1 {:is-active true})
|
(let [prof (th/create-profile* 1 {:is-active true})
|
||||||
team-id (:default-team-id prof)
|
team-id (:default-team-id prof)
|
||||||
proj-id (:default-project-id prof)
|
proj-id (:default-project-id prof)
|
||||||
file-id (uuid/next)
|
page-id (uuid/next)
|
||||||
page-id (uuid/next)]
|
data {::th/type :create-file
|
||||||
|
|
||||||
(t/testing "create file"
|
|
||||||
(let [data {::th/type :create-file
|
|
||||||
::rpc/profile-id (:id prof)
|
::rpc/profile-id (:id prof)
|
||||||
:project-id proj-id
|
:project-id proj-id
|
||||||
:id file-id
|
|
||||||
:name "foobar"
|
:name "foobar"
|
||||||
:is-shared false
|
:is-shared false}
|
||||||
:components-v2 true}
|
out (th/command! data)
|
||||||
out (th/command! data)]
|
file-id (:id (:result out))]
|
||||||
|
|
||||||
|
(t/testing "create file"
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (nil? (:error out)))
|
(t/is (nil? (:error out)))
|
||||||
|
|
||||||
(let [result (:result out)]
|
(let [result (:result out)]
|
||||||
|
(t/is (uuid? file-id))
|
||||||
(t/is (= (:name data) (:name result)))
|
(t/is (= (:name data) (:name result)))
|
||||||
(t/is (= proj-id (:project-id result))))))
|
(t/is (= proj-id (:project-id result)))))
|
||||||
|
|
||||||
(t/testing "rename file"
|
(t/testing "rename file"
|
||||||
(let [data {::th/type :rename-file
|
(let [data {::th/type :rename-file
|
||||||
@ -141,58 +139,6 @@
|
|||||||
(let [result (:result out)]
|
(let [result (:result out)]
|
||||||
(t/is (= 0 (count result))))))))
|
(t/is (= 0 (count result))))))))
|
||||||
|
|
||||||
(t/deftest create-file-with-duplicate-id
|
|
||||||
(let [prof (th/create-profile* 1 {:is-active true})
|
|
||||||
proj-id (:default-project-id prof)
|
|
||||||
file-id (uuid/next)]
|
|
||||||
|
|
||||||
(t/testing "create file with specific id"
|
|
||||||
(let [data {::th/type :create-file
|
|
||||||
::rpc/profile-id (:id prof)
|
|
||||||
:project-id proj-id
|
|
||||||
:id file-id
|
|
||||||
:name "first-file"}
|
|
||||||
out (th/command! data)]
|
|
||||||
(t/is (nil? (:error out)))))
|
|
||||||
|
|
||||||
(t/testing "create file with duplicate id returns normalized error"
|
|
||||||
(let [data {::th/type :create-file
|
|
||||||
::rpc/profile-id (:id prof)
|
|
||||||
:project-id proj-id
|
|
||||||
:id file-id
|
|
||||||
:name "duplicate-file"}
|
|
||||||
out (th/command! data)
|
|
||||||
err (:error out)]
|
|
||||||
(t/is (th/ex-info? err))
|
|
||||||
(t/is (th/ex-of-type? err :not-found))))))
|
|
||||||
|
|
||||||
(t/deftest create-file-id-version
|
|
||||||
(let [prof (th/create-profile* 1 {:is-active true})
|
|
||||||
proj-id (:default-project-id prof)
|
|
||||||
v3-id "6fa459ea-ee8a-3ca4-894e-db77e160355e"
|
|
||||||
v4-id "550e8400-e29b-41d4-a716-446655440000"]
|
|
||||||
|
|
||||||
;; reserved version (v3) must be rejected at the RPC boundary
|
|
||||||
(let [data {::th/type :create-file
|
|
||||||
::rpc/profile-id (:id prof)
|
|
||||||
:project-id proj-id
|
|
||||||
:id v3-id
|
|
||||||
:name "file with v3 id"}
|
|
||||||
out (th/command! data)]
|
|
||||||
(t/is (not (th/success? out)))
|
|
||||||
(t/is (th/ex-of-type? (:error out) :validation))
|
|
||||||
(t/is (th/ex-of-code? (:error out) :params-validation)))
|
|
||||||
|
|
||||||
;; v4 id is accepted
|
|
||||||
(let [data {::th/type :create-file
|
|
||||||
::rpc/profile-id (:id prof)
|
|
||||||
:project-id proj-id
|
|
||||||
:id v4-id
|
|
||||||
:name "file with v4 id"}
|
|
||||||
out (th/command! data)]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
(t/is (= v4-id (str (:id (:result out))))))))
|
|
||||||
|
|
||||||
(t/deftest file-gc-with-fragments
|
(t/deftest file-gc-with-fragments
|
||||||
(let [profile (th/create-profile* 1)
|
(let [profile (th/create-profile* 1)
|
||||||
file (th/create-file* 1 {:profile-id (:id profile)
|
file (th/create-file* 1 {:profile-id (:id profile)
|
||||||
@ -824,8 +770,7 @@
|
|||||||
::rpc/profile-id (:id profile2)
|
::rpc/profile-id (:id profile2)
|
||||||
:project-id (:default-project-id profile1)
|
:project-id (:default-project-id profile1)
|
||||||
:name "foobar"
|
:name "foobar"
|
||||||
:is-shared false
|
:is-shared false}
|
||||||
:components-v2 true}
|
|
||||||
out (th/command! data)
|
out (th/command! data)
|
||||||
error (:error out)]
|
error (:error out)]
|
||||||
|
|
||||||
@ -2118,25 +2063,22 @@
|
|||||||
(let [prof (th/create-profile* 1 {:is-active true})
|
(let [prof (th/create-profile* 1 {:is-active true})
|
||||||
team-id (:default-team-id prof)
|
team-id (:default-team-id prof)
|
||||||
proj-id (:default-project-id prof)
|
proj-id (:default-project-id prof)
|
||||||
file-id (uuid/next)
|
|
||||||
now (ct/inst "2025-10-31T00:00:00Z")]
|
now (ct/inst "2025-10-31T00:00:00Z")]
|
||||||
|
|
||||||
(binding [ct/*clock* (ct/fixed-clock now)]
|
(binding [ct/*clock* (ct/fixed-clock now)]
|
||||||
(let [data {::th/type :create-file
|
(let [data {::th/type :create-file
|
||||||
::rpc/profile-id (:id prof)
|
::rpc/profile-id (:id prof)
|
||||||
:project-id proj-id
|
:project-id proj-id
|
||||||
:id file-id
|
|
||||||
:name "foobar"
|
:name "foobar"
|
||||||
:is-shared false
|
:is-shared false}
|
||||||
:components-v2 true}
|
out (th/command! data)
|
||||||
out (th/command! data)]
|
_ (t/is (nil? (:error out)))
|
||||||
|
file-id (:id (:result out))]
|
||||||
|
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (nil? (:error out)))
|
|
||||||
|
|
||||||
(let [result (:result out)]
|
(let [result (:result out)]
|
||||||
(t/is (= (:name data) (:name result)))
|
(t/is (= (:name data) (:name result)))
|
||||||
(t/is (= proj-id (:project-id result)))))
|
(t/is (= proj-id (:project-id result))))
|
||||||
|
|
||||||
(let [data {::th/type :delete-file
|
(let [data {::th/type :delete-file
|
||||||
:id file-id
|
:id file-id
|
||||||
@ -2174,31 +2116,28 @@
|
|||||||
(t/is (= (:ids data) (val ev2)))))
|
(t/is (= (:ids data) (val ev2)))))
|
||||||
|
|
||||||
(let [row (th/db-exec-one! ["select * from file where id = ?" file-id])]
|
(let [row (th/db-exec-one! ["select * from file where id = ?" file-id])]
|
||||||
(t/is (= (:deleted-at row) now)))))))
|
(t/is (= (:deleted-at row) now))))))))
|
||||||
|
|
||||||
(t/deftest restore-deleted-files
|
(t/deftest restore-deleted-files
|
||||||
(let [prof (th/create-profile* 1 {:is-active true})
|
(let [prof (th/create-profile* 1 {:is-active true})
|
||||||
team-id (:default-team-id prof)
|
team-id (:default-team-id prof)
|
||||||
proj-id (:default-project-id prof)
|
proj-id (:default-project-id prof)
|
||||||
file-id (uuid/next)
|
|
||||||
now (ct/inst "2025-10-31T00:00:00Z")]
|
now (ct/inst "2025-10-31T00:00:00Z")]
|
||||||
|
|
||||||
(binding [ct/*clock* (ct/fixed-clock now)]
|
(binding [ct/*clock* (ct/fixed-clock now)]
|
||||||
(let [data {::th/type :create-file
|
(let [data {::th/type :create-file
|
||||||
::rpc/profile-id (:id prof)
|
::rpc/profile-id (:id prof)
|
||||||
:project-id proj-id
|
:project-id proj-id
|
||||||
:id file-id
|
|
||||||
:name "foobar"
|
:name "foobar"
|
||||||
:is-shared false
|
:is-shared false}
|
||||||
:components-v2 true}
|
out (th/command! data)
|
||||||
out (th/command! data)]
|
_ (t/is (nil? (:error out)))
|
||||||
|
file-id (:id (:result out))]
|
||||||
|
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (nil? (:error out)))
|
|
||||||
|
|
||||||
(let [result (:result out)]
|
(let [result (:result out)]
|
||||||
(t/is (= (:name data) (:name result)))
|
(t/is (= (:name data) (:name result)))
|
||||||
(t/is (= proj-id (:project-id result)))))
|
(t/is (= proj-id (:project-id result))))
|
||||||
|
|
||||||
(let [data {::th/type :delete-file
|
(let [data {::th/type :delete-file
|
||||||
:id file-id
|
:id file-id
|
||||||
@ -2238,7 +2177,7 @@
|
|||||||
(t/is (= (:ids data) (last (last events)))))))
|
(t/is (= (:ids data) (last (last events)))))))
|
||||||
|
|
||||||
(let [row (th/db-exec-one! ["select * from file where id = ?" file-id])]
|
(let [row (th/db-exec-one! ["select * from file where id = ?" file-id])]
|
||||||
(t/is (nil? (:deleted-at row)))))))
|
(t/is (nil? (:deleted-at row))))))))
|
||||||
|
|
||||||
|
|
||||||
(t/deftest restore-deleted-files-and-projets
|
(t/deftest restore-deleted-files-and-projets
|
||||||
@ -2721,3 +2660,15 @@
|
|||||||
(t/is (th/ex-info? err))
|
(t/is (th/ex-info? err))
|
||||||
(t/is (= :not-found (:type edata)))
|
(t/is (= :not-found (:type edata)))
|
||||||
(t/is (= :object-not-found (:code edata)))))))
|
(t/is (= :object-not-found (:code edata)))))))
|
||||||
|
|
||||||
|
(t/deftest create-file-rejects-client-id
|
||||||
|
(let [prof (th/create-profile* 1 {:is-active true})
|
||||||
|
sent-id (uuid/next)
|
||||||
|
out (th/command! {::th/type :create-file
|
||||||
|
::rpc/profile-id (:id prof)
|
||||||
|
:project-id (:default-project-id prof)
|
||||||
|
:name "file with client id"
|
||||||
|
:id sent-id})]
|
||||||
|
(t/is (th/ex-info? (:error out)))
|
||||||
|
(t/is (th/ex-of-type? (:error out) :validation))
|
||||||
|
(t/is (th/ex-of-code? (:error out) :params-validation))))
|
||||||
|
|||||||
@ -79,6 +79,7 @@
|
|||||||
(t/is (nil? (:error out)))
|
(t/is (nil? (:error out)))
|
||||||
(let [{:keys [media-id thumbnail-id] :as result} (:result out)]
|
(let [{:keys [media-id thumbnail-id] :as result} (:result out)]
|
||||||
(t/is (= (:id file) (:file-id result)))
|
(t/is (= (:id file) (:file-id result)))
|
||||||
|
(t/is (uuid? (:id result)))
|
||||||
(t/is (= 800 (:width result)))
|
(t/is (= 800 (:width result)))
|
||||||
(t/is (= 800 (:height result)))
|
(t/is (= 800 (:height result)))
|
||||||
(t/is (= "image/jpeg" (:mtype result)))
|
(t/is (= "image/jpeg" (:mtype result)))
|
||||||
@ -93,111 +94,6 @@
|
|||||||
(t/is (= 3890 (:size mobj2)))))))
|
(t/is (= 3890 (:size mobj2)))))))
|
||||||
|
|
||||||
|
|
||||||
(t/deftest media-object-upload-idempotency
|
|
||||||
(let [prof (th/create-profile* 1)
|
|
||||||
proj (th/create-project* 1 {:profile-id (:id prof)
|
|
||||||
:team-id (:default-team-id prof)})
|
|
||||||
file (th/create-file* 1 {:profile-id (:id prof)
|
|
||||||
:project-id (:default-project-id prof)
|
|
||||||
:is-shared false})
|
|
||||||
mfile {:filename "sample.jpg"
|
|
||||||
:path (th/tempfile "backend_tests/test_files/sample.jpg")
|
|
||||||
:mtype "image/jpeg"
|
|
||||||
:size 312043}
|
|
||||||
|
|
||||||
params {::th/type :upload-file-media-object
|
|
||||||
::rpc/profile-id (:id prof)
|
|
||||||
:file-id (:id file)
|
|
||||||
:is-local true
|
|
||||||
:name "testfile"
|
|
||||||
:content mfile
|
|
||||||
:id (uuid/next)}]
|
|
||||||
|
|
||||||
;; First try
|
|
||||||
(let [{:keys [result error] :as out} (th/command! params)]
|
|
||||||
;; (th/print-result! out)
|
|
||||||
(t/is (nil? error))
|
|
||||||
(t/is (= (:id params) (:id result)))
|
|
||||||
(t/is (= (:file-id params) (:file-id result)))
|
|
||||||
(t/is (= 800 (:width result)))
|
|
||||||
(t/is (= 800 (:height result)))
|
|
||||||
(t/is (= "image/jpeg" (:mtype result)))
|
|
||||||
(t/is (uuid? (:media-id result)))
|
|
||||||
(t/is (uuid? (:thumbnail-id result))))
|
|
||||||
|
|
||||||
;; Second try
|
|
||||||
(let [{:keys [result error] :as out} (th/command! params)]
|
|
||||||
;; (th/print-result! out)
|
|
||||||
(t/is (nil? error))
|
|
||||||
(t/is (= (:id params) (:id result)))
|
|
||||||
(t/is (= (:file-id params) (:file-id result)))
|
|
||||||
(t/is (= 800 (:width result)))
|
|
||||||
(t/is (= 800 (:height result)))
|
|
||||||
(t/is (= "image/jpeg" (:mtype result)))
|
|
||||||
(t/is (uuid? (:media-id result)))
|
|
||||||
(t/is (uuid? (:thumbnail-id result))))))
|
|
||||||
|
|
||||||
|
|
||||||
(t/deftest upload-file-media-object-id-version
|
|
||||||
(let [prof (th/create-profile* 1)
|
|
||||||
_ (th/create-project* 1 {:profile-id (:id prof)
|
|
||||||
:team-id (:default-team-id prof)})
|
|
||||||
file (th/create-file* 1 {:profile-id (:id prof)
|
|
||||||
:project-id (:default-project-id prof)
|
|
||||||
:is-shared false})
|
|
||||||
mfile {:filename "sample.jpg"
|
|
||||||
:path (th/tempfile "backend_tests/test_files/sample.jpg")
|
|
||||||
:mtype "image/jpeg"
|
|
||||||
:size 312043}
|
|
||||||
v3-id "6fa459ea-ee8a-3ca4-894e-db77e160355e"
|
|
||||||
v4-id "550e8400-e29b-41d4-a716-446655440000"]
|
|
||||||
|
|
||||||
;; reserved version (v3) must be rejected at the RPC boundary
|
|
||||||
(let [params {::th/type :upload-file-media-object
|
|
||||||
::rpc/profile-id (:id prof)
|
|
||||||
:file-id (:id file)
|
|
||||||
:is-local true
|
|
||||||
:name "testfile"
|
|
||||||
:content mfile
|
|
||||||
:id v3-id}
|
|
||||||
out (th/command! params)]
|
|
||||||
(t/is (not (th/success? out)))
|
|
||||||
(t/is (th/ex-of-type? (:error out) :validation))
|
|
||||||
(t/is (th/ex-of-code? (:error out) :params-validation)))
|
|
||||||
|
|
||||||
;; v4 id is accepted
|
|
||||||
(let [params {::th/type :upload-file-media-object
|
|
||||||
::rpc/profile-id (:id prof)
|
|
||||||
:file-id (:id file)
|
|
||||||
:is-local true
|
|
||||||
:name "testfile"
|
|
||||||
:content mfile
|
|
||||||
:id v4-id}
|
|
||||||
out (th/command! params)]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
(t/is (= v4-id (str (:id (:result out))))))))
|
|
||||||
|
|
||||||
(t/deftest create-file-media-object-from-url-id-version
|
|
||||||
(let [prof (th/create-profile* 1)
|
|
||||||
_ (th/create-project* 1 {:profile-id (:id prof)
|
|
||||||
:team-id (:default-team-id prof)})
|
|
||||||
file (th/create-file* 1 {:profile-id (:id prof)
|
|
||||||
:project-id (:default-project-id prof)
|
|
||||||
:is-shared false})
|
|
||||||
v3-id "6fa459ea-ee8a-3ca4-894e-db77e160355e"]
|
|
||||||
|
|
||||||
;; reserved version (v3) must be rejected before any download happens
|
|
||||||
(let [params {::th/type :create-file-media-object-from-url
|
|
||||||
::rpc/profile-id (:id prof)
|
|
||||||
:file-id (:id file)
|
|
||||||
:is-local true
|
|
||||||
:url "https://example.com/sample.jpg"
|
|
||||||
:id v3-id}
|
|
||||||
out (th/command! params)]
|
|
||||||
(t/is (not (th/success? out)))
|
|
||||||
(t/is (th/ex-of-type? (:error out) :validation))
|
|
||||||
(t/is (th/ex-of-code? (:error out) :params-validation)))))
|
|
||||||
|
|
||||||
(t/deftest media-object-from-url-command
|
(t/deftest media-object-from-url-command
|
||||||
(let [prof (th/create-profile* 1)
|
(let [prof (th/create-profile* 1)
|
||||||
proj (th/create-project* 1 {:profile-id (:id prof)
|
proj (th/create-project* 1 {:profile-id (:id prof)
|
||||||
@ -254,6 +150,7 @@
|
|||||||
(t/is (nil? (:error out)))
|
(t/is (nil? (:error out)))
|
||||||
(let [{:keys [media-id thumbnail-id] :as result} (:result out)]
|
(let [{:keys [media-id thumbnail-id] :as result} (:result out)]
|
||||||
(t/is (= (:id file) (:file-id result)))
|
(t/is (= (:id file) (:file-id result)))
|
||||||
|
(t/is (uuid? (:id result)))
|
||||||
(t/is (= 800 (:width result)))
|
(t/is (= 800 (:width result)))
|
||||||
(t/is (= 800 (:height result)))
|
(t/is (= 800 (:height result)))
|
||||||
(t/is (= "image/jpeg" (:mtype result)))
|
(t/is (= "image/jpeg" (:mtype result)))
|
||||||
@ -268,51 +165,6 @@
|
|||||||
(t/is (= 3890 (:size mobj2)))))))
|
(t/is (= 3890 (:size mobj2)))))))
|
||||||
|
|
||||||
|
|
||||||
(t/deftest media-object-upload-idempotency-command
|
|
||||||
(let [prof (th/create-profile* 1)
|
|
||||||
proj (th/create-project* 1 {:profile-id (:id prof)
|
|
||||||
:team-id (:default-team-id prof)})
|
|
||||||
file (th/create-file* 1 {:profile-id (:id prof)
|
|
||||||
:project-id (:default-project-id prof)
|
|
||||||
:is-shared false})
|
|
||||||
mfile {:filename "sample.jpg"
|
|
||||||
:path (th/tempfile "backend_tests/test_files/sample.jpg")
|
|
||||||
:mtype "image/jpeg"
|
|
||||||
:size 312043}
|
|
||||||
|
|
||||||
params {::th/type :upload-file-media-object
|
|
||||||
::rpc/profile-id (:id prof)
|
|
||||||
:file-id (:id file)
|
|
||||||
:is-local true
|
|
||||||
:name "testfile"
|
|
||||||
:content mfile
|
|
||||||
:id (uuid/next)}]
|
|
||||||
|
|
||||||
;; First try
|
|
||||||
(let [{:keys [result error] :as out} (th/command! params)]
|
|
||||||
;; (th/print-result! out)
|
|
||||||
(t/is (nil? error))
|
|
||||||
(t/is (= (:id params) (:id result)))
|
|
||||||
(t/is (= (:file-id params) (:file-id result)))
|
|
||||||
(t/is (= 800 (:width result)))
|
|
||||||
(t/is (= 800 (:height result)))
|
|
||||||
(t/is (= "image/jpeg" (:mtype result)))
|
|
||||||
(t/is (uuid? (:media-id result)))
|
|
||||||
(t/is (uuid? (:thumbnail-id result))))
|
|
||||||
|
|
||||||
;; Second try
|
|
||||||
(let [{:keys [result error] :as out} (th/command! params)]
|
|
||||||
;; (th/print-result! out)
|
|
||||||
(t/is (nil? error))
|
|
||||||
(t/is (= (:id params) (:id result)))
|
|
||||||
(t/is (= (:file-id params) (:file-id result)))
|
|
||||||
(t/is (= 800 (:width result)))
|
|
||||||
(t/is (= 800 (:height result)))
|
|
||||||
(t/is (= "image/jpeg" (:mtype result)))
|
|
||||||
(t/is (uuid? (:media-id result)))
|
|
||||||
(t/is (uuid? (:thumbnail-id result))))))
|
|
||||||
|
|
||||||
|
|
||||||
(t/deftest media-object-upload-command-when-file-is-deleted
|
(t/deftest media-object-upload-command-when-file-is-deleted
|
||||||
(let [prof (th/create-profile* 1)
|
(let [prof (th/create-profile* 1)
|
||||||
proj (th/create-project* 1 {:profile-id (:id prof)
|
proj (th/create-project* 1 {:profile-id (:id prof)
|
||||||
@ -580,7 +432,6 @@
|
|||||||
file (th/create-file* 1 {:profile-id (:id prof)
|
file (th/create-file* 1 {:profile-id (:id prof)
|
||||||
:project-id (:default-project-id prof)
|
:project-id (:default-project-id prof)
|
||||||
:is-shared false})
|
:is-shared false})
|
||||||
media-id (uuid/next)
|
|
||||||
source-path (th/tempfile "backend_tests/test_files/sample.jpg")
|
source-path (th/tempfile "backend_tests/test_files/sample.jpg")
|
||||||
chunks (split-file-into-chunks source-path 312043) ; single chunk = whole file
|
chunks (split-file-into-chunks source-path 312043) ; single chunk = whole file
|
||||||
mtype "image/jpeg"
|
mtype "image/jpeg"
|
||||||
@ -600,10 +451,9 @@
|
|||||||
:file-id (:id file)
|
:file-id (:id file)
|
||||||
:is-local true
|
:is-local true
|
||||||
:name "sample"
|
:name "sample"
|
||||||
:mtype mtype
|
:mtype mtype})]
|
||||||
:id media-id})]
|
|
||||||
(t/is (nil? (:error out1)))
|
(t/is (nil? (:error out1)))
|
||||||
(t/is (= media-id (:id (:result out1)))))
|
(t/is (uuid? (:id (:result out1)))))
|
||||||
|
|
||||||
;; Second assemble with the same session-id must fail because the
|
;; Second assemble with the same session-id must fail because the
|
||||||
;; session row has been marked as consumed after the first assembly
|
;; session row has been marked as consumed after the first assembly
|
||||||
@ -613,8 +463,7 @@
|
|||||||
:file-id (:id file)
|
:file-id (:id file)
|
||||||
:is-local true
|
:is-local true
|
||||||
:name "sample"
|
:name "sample"
|
||||||
:mtype mtype
|
:mtype mtype})]
|
||||||
:id media-id})]
|
|
||||||
(t/is (some? (:error out2)))
|
(t/is (some? (:error out2)))
|
||||||
(t/is (= :not-found (-> out2 :error ex-data :type)))
|
(t/is (= :not-found (-> out2 :error ex-data :type)))
|
||||||
(t/is (= :object-not-found (-> out2 :error ex-data :code))))))
|
(t/is (= :object-not-found (-> out2 :error ex-data :code))))))
|
||||||
@ -1323,47 +1172,57 @@
|
|||||||
(t/is (= :not-found (:type error-data)))
|
(t/is (= :not-found (:type error-data)))
|
||||||
(t/is (= :object-not-found (:code error-data))))))
|
(t/is (= :object-not-found (:code error-data))))))
|
||||||
|
|
||||||
(t/deftest assemble-file-media-object-id-version
|
(t/deftest upload-file-media-object-rejects-client-id
|
||||||
(let [prof (th/create-profile* 1)
|
(let [prof (th/create-profile* 1)
|
||||||
_ (th/create-project* 1 {:profile-id (:id prof)
|
|
||||||
:team-id (:default-team-id prof)})
|
|
||||||
file (th/create-file* 1 {:profile-id (:id prof)
|
file (th/create-file* 1 {:profile-id (:id prof)
|
||||||
:project-id (:default-project-id prof)
|
:project-id (:default-project-id prof)
|
||||||
:is-shared false})
|
:is-shared false})
|
||||||
v3-id "6fa459ea-ee8a-3ca4-894e-db77e160355e"
|
mfile {:filename "sample.jpg"
|
||||||
v4-id "550e8400-e29b-41d4-a716-446655440000"
|
:path (th/tempfile "backend_tests/test_files/sample.jpg")
|
||||||
mtype "image/jpeg"]
|
:mtype "image/jpeg"
|
||||||
|
:size 312043}
|
||||||
|
sent-id (uuid/next)
|
||||||
|
out (th/command! {::th/type :upload-file-media-object
|
||||||
|
::rpc/profile-id (:id prof)
|
||||||
|
:file-id (:id file)
|
||||||
|
:is-local true
|
||||||
|
:name "testfile"
|
||||||
|
:content mfile
|
||||||
|
:id sent-id})]
|
||||||
|
(t/is (th/ex-info? (:error out)))
|
||||||
|
(t/is (th/ex-of-type? (:error out) :validation))
|
||||||
|
(t/is (th/ex-of-code? (:error out) :params-validation))))
|
||||||
|
|
||||||
;; reserved version (v3) must be rejected without touching any session
|
(t/deftest create-file-media-object-from-url-rejects-client-id
|
||||||
(let [out (th/command! {::th/type :assemble-file-media-object
|
(let [prof (th/create-profile* 1)
|
||||||
|
file (th/create-file* 1 {:profile-id (:id prof)
|
||||||
|
:project-id (:default-project-id prof)
|
||||||
|
:is-shared false})
|
||||||
|
sent-id (uuid/next)
|
||||||
|
out (th/command! {::th/type :create-file-media-object-from-url
|
||||||
|
::rpc/profile-id (:id prof)
|
||||||
|
:file-id (:id file)
|
||||||
|
:is-local true
|
||||||
|
:url "https://example.com/sample.jpg"
|
||||||
|
:id sent-id})]
|
||||||
|
(t/is (th/ex-info? (:error out)))
|
||||||
|
(t/is (th/ex-of-type? (:error out) :validation))
|
||||||
|
(t/is (th/ex-of-code? (:error out) :params-validation))))
|
||||||
|
|
||||||
|
(t/deftest assemble-file-media-object-rejects-client-id
|
||||||
|
(let [prof (th/create-profile* 1)
|
||||||
|
file (th/create-file* 1 {:profile-id (:id prof)
|
||||||
|
:project-id (:default-project-id prof)
|
||||||
|
:is-shared false})
|
||||||
|
sent-id (uuid/next)
|
||||||
|
out (th/command! {::th/type :assemble-file-media-object
|
||||||
::rpc/profile-id (:id prof)
|
::rpc/profile-id (:id prof)
|
||||||
:session-id (uuid/next)
|
:session-id (uuid/next)
|
||||||
:file-id (:id file)
|
:file-id (:id file)
|
||||||
:is-local true
|
:is-local true
|
||||||
:name "assembled-image"
|
:name "assembled-image"
|
||||||
:mtype mtype
|
:mtype "image/jpeg"
|
||||||
:id v3-id})]
|
:id sent-id})]
|
||||||
(t/is (not (th/success? out)))
|
(t/is (th/ex-info? (:error out)))
|
||||||
(t/is (th/ex-of-type? (:error out) :validation))
|
(t/is (th/ex-of-type? (:error out) :validation))
|
||||||
(t/is (th/ex-of-code? (:error out) :params-validation)))
|
(t/is (th/ex-of-code? (:error out) :params-validation))))
|
||||||
|
|
||||||
;; v4 id is accepted through the full chunked flow
|
|
||||||
(let [source-path (th/tempfile "backend_tests/test_files/sample.jpg")
|
|
||||||
chunks (split-file-into-chunks source-path 312043)
|
|
||||||
session-id (create-session! prof 1)
|
|
||||||
mfile (make-chunk-mfile (first chunks) mtype)
|
|
||||||
_ (th/command! {::th/type :upload-chunk
|
|
||||||
::rpc/profile-id (:id prof)
|
|
||||||
:session-id session-id
|
|
||||||
:index 0
|
|
||||||
:content mfile})
|
|
||||||
out (th/command! {::th/type :assemble-file-media-object
|
|
||||||
::rpc/profile-id (:id prof)
|
|
||||||
:session-id session-id
|
|
||||||
:file-id (:id file)
|
|
||||||
:is-local true
|
|
||||||
:name "assembled-image"
|
|
||||||
:mtype mtype
|
|
||||||
:id v4-id})]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
(t/is (= v4-id (str (:id (:result out))))))))
|
|
||||||
|
|||||||
@ -21,20 +21,19 @@
|
|||||||
(t/deftest projects-simple-crud
|
(t/deftest projects-simple-crud
|
||||||
(let [profile (th/create-profile* 1)
|
(let [profile (th/create-profile* 1)
|
||||||
team (th/create-team* 1 {:profile-id (:id profile)})
|
team (th/create-team* 1 {:profile-id (:id profile)})
|
||||||
project-id (uuid/next)]
|
data {::th/type :create-project
|
||||||
|
|
||||||
;; create project
|
|
||||||
(let [data {::th/type :create-project
|
|
||||||
::rpc/profile-id (:id profile)
|
::rpc/profile-id (:id profile)
|
||||||
:id project-id
|
|
||||||
:team-id (:id team)
|
:team-id (:id team)
|
||||||
:name "test project"}
|
:name "test project"}
|
||||||
out (th/command! data)]
|
out (th/command! data)
|
||||||
;; (th/print-result! out)
|
_ (t/is (nil? (:error out)))
|
||||||
|
project-id (:id (:result out))]
|
||||||
|
|
||||||
(t/is (nil? (:error out)))
|
;; create project
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(t/is (uuid? project-id))
|
||||||
(let [result (:result out)]
|
(let [result (:result out)]
|
||||||
(t/is (= (:name data) (:name result)))))
|
(t/is (= (:name data) (:name result))))
|
||||||
|
|
||||||
;; query the list of projects of a team
|
;; query the list of projects of a team
|
||||||
(let [data {::th/type :get-projects
|
(let [data {::th/type :get-projects
|
||||||
@ -123,33 +122,6 @@
|
|||||||
(t/is (th/ex-info? error))
|
(t/is (th/ex-info? error))
|
||||||
(t/is (th/ex-of-type? error :not-found))))
|
(t/is (th/ex-of-type? error :not-found))))
|
||||||
|
|
||||||
(t/deftest create-project-id-version
|
|
||||||
(let [profile (th/create-profile* 1)
|
|
||||||
team (th/create-team* 1 {:profile-id (:id profile)})
|
|
||||||
v3-id "6fa459ea-ee8a-3ca4-894e-db77e160355e"
|
|
||||||
v4-id "550e8400-e29b-41d4-a716-446655440000"]
|
|
||||||
|
|
||||||
;; reserved version (v3) must be rejected at the RPC boundary
|
|
||||||
(let [data {::th/type :create-project
|
|
||||||
::rpc/profile-id (:id profile)
|
|
||||||
:team-id (:id team)
|
|
||||||
:id v3-id
|
|
||||||
:name "project with v3 id"}
|
|
||||||
out (th/command! data)]
|
|
||||||
(t/is (not (th/success? out)))
|
|
||||||
(t/is (th/ex-of-type? (:error out) :validation))
|
|
||||||
(t/is (th/ex-of-code? (:error out) :params-validation)))
|
|
||||||
|
|
||||||
;; v4 id is accepted
|
|
||||||
(let [data {::th/type :create-project
|
|
||||||
::rpc/profile-id (:id profile)
|
|
||||||
:team-id (:id team)
|
|
||||||
:id v4-id
|
|
||||||
:name "project with v4 id"}
|
|
||||||
out (th/command! data)]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
(t/is (= v4-id (str (:id (:result out))))))))
|
|
||||||
|
|
||||||
(t/deftest permissions-checks-rename-project
|
(t/deftest permissions-checks-rename-project
|
||||||
(let [profile1 (th/create-profile* 1)
|
(let [profile1 (th/create-profile* 1)
|
||||||
profile2 (th/create-profile* 2)
|
profile2 (th/create-profile* 2)
|
||||||
@ -289,3 +261,16 @@
|
|||||||
err (:error out)]
|
err (:error out)]
|
||||||
(t/is (th/ex-info? err))
|
(t/is (th/ex-info? err))
|
||||||
(t/is (th/ex-of-type? err :not-found))))
|
(t/is (th/ex-of-type? err :not-found))))
|
||||||
|
|
||||||
|
(t/deftest create-project-rejects-client-id
|
||||||
|
(let [profile (th/create-profile* 1 {:is-active true})
|
||||||
|
team (th/create-team* 1 {:profile-id (:id profile)})
|
||||||
|
sent-id (uuid/next)
|
||||||
|
out (th/command! {::th/type :create-project
|
||||||
|
::rpc/profile-id (:id profile)
|
||||||
|
:team-id (:id team)
|
||||||
|
:name "project with client id"
|
||||||
|
:id sent-id})]
|
||||||
|
(t/is (th/ex-info? (:error out)))
|
||||||
|
(t/is (th/ex-of-type? (:error out) :validation))
|
||||||
|
(t/is (th/ex-of-code? (:error out) :params-validation))))
|
||||||
|
|||||||
@ -1116,62 +1116,6 @@
|
|||||||
out (th/command! data)]
|
out (th/command! data)]
|
||||||
(t/is (th/success? out)))))
|
(t/is (th/success? out)))))
|
||||||
|
|
||||||
(t/deftest create-team-id-version
|
|
||||||
(let [profile (th/create-profile* 1 {:is-active true})
|
|
||||||
v3-id "6fa459ea-ee8a-3ca4-894e-db77e160355e"
|
|
||||||
v4-id "550e8400-e29b-41d4-a716-446655440000"
|
|
||||||
v7-id "0191062e-3f50-7a5e-9f5a-1a2b3c4d5e6f"]
|
|
||||||
|
|
||||||
;; reserved version (v3) must be rejected at the RPC boundary
|
|
||||||
(let [data {::th/type :create-team
|
|
||||||
::rpc/profile-id (:id profile)
|
|
||||||
:name "team with v3 id"
|
|
||||||
:id v3-id}
|
|
||||||
out (th/command! data)]
|
|
||||||
(t/is (not (th/success? out)))
|
|
||||||
(t/is (th/ex-of-type? (:error out) :validation))
|
|
||||||
(t/is (th/ex-of-code? (:error out) :params-validation)))
|
|
||||||
|
|
||||||
;; v4, v7 and v8 ids are accepted
|
|
||||||
(doseq [id [v4-id v7-id (str (uuid/next))]]
|
|
||||||
(let [data {::th/type :create-team
|
|
||||||
::rpc/profile-id (:id profile)
|
|
||||||
:name (str "team with id " id)
|
|
||||||
:id id}
|
|
||||||
out (th/command! data)]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
(t/is (= id (str (:id (:result out)))))))))
|
|
||||||
|
|
||||||
(t/deftest create-team-with-invitations-id-version
|
|
||||||
(with-mocks [mock {:target 'app.email/send! :return nil}]
|
|
||||||
(let [profile (th/create-profile* 1 {:is-active true})
|
|
||||||
v3-id "6fa459ea-ee8a-3ca4-894e-db77e160355e"
|
|
||||||
v4-id "550e8400-e29b-41d4-a716-446655440000"]
|
|
||||||
|
|
||||||
;; reserved version (v3) must be rejected before any invitation is sent
|
|
||||||
(let [data {::th/type :create-team-with-invitations
|
|
||||||
::rpc/profile-id (:id profile)
|
|
||||||
:name "team with v3 id"
|
|
||||||
:id v3-id
|
|
||||||
:emails #{"invitee@example.com"}
|
|
||||||
:role :editor}
|
|
||||||
out (th/command! data)]
|
|
||||||
(t/is (not (th/success? out)))
|
|
||||||
(t/is (th/ex-of-type? (:error out) :validation))
|
|
||||||
(t/is (th/ex-of-code? (:error out) :params-validation))
|
|
||||||
(t/is (= 0 (:call-count @mock))))
|
|
||||||
|
|
||||||
;; v4 id is accepted
|
|
||||||
(let [data {::th/type :create-team-with-invitations
|
|
||||||
::rpc/profile-id (:id profile)
|
|
||||||
:name "team with v4 id"
|
|
||||||
:id v4-id
|
|
||||||
:emails #{"invitee@example.com"}
|
|
||||||
:role :editor}
|
|
||||||
out (th/command! data)]
|
|
||||||
(t/is (th/success? out))
|
|
||||||
(t/is (= v4-id (str (:id (:result out)))))))))
|
|
||||||
|
|
||||||
(t/deftest create-team-invitations-email-cooldown
|
(t/deftest create-team-invitations-email-cooldown
|
||||||
(with-mocks [mock {:target 'app.email/send! :return nil}]
|
(with-mocks [mock {:target 'app.email/send! :return nil}]
|
||||||
(let [profile1 (th/create-profile* 1 {:is-active true})
|
(let [profile1 (th/create-profile* 1 {:is-active true})
|
||||||
@ -1482,3 +1426,27 @@
|
|||||||
(t/is (not (th/success? out)))
|
(t/is (not (th/success? out)))
|
||||||
(t/is (th/ex-of-type? (:error out) :not-found))
|
(t/is (th/ex-of-type? (:error out) :not-found))
|
||||||
(t/is (th/ex-of-code? (:error out) :member-does-not-exist)))))
|
(t/is (th/ex-of-code? (:error out) :member-does-not-exist)))))
|
||||||
|
|
||||||
|
(t/deftest create-team-rejects-client-id
|
||||||
|
(let [profile (th/create-profile* 1 {:is-active true})
|
||||||
|
sent-id (uuid/next)
|
||||||
|
out (th/command! {::th/type :create-team
|
||||||
|
::rpc/profile-id (:id profile)
|
||||||
|
:name "team with client id"
|
||||||
|
:id sent-id})]
|
||||||
|
(t/is (th/ex-info? (:error out)))
|
||||||
|
(t/is (th/ex-of-type? (:error out) :validation))
|
||||||
|
(t/is (th/ex-of-code? (:error out) :params-validation))))
|
||||||
|
|
||||||
|
(t/deftest create-team-with-invitations-rejects-client-id
|
||||||
|
(let [profile (th/create-profile* 1 {:is-active true})
|
||||||
|
sent-id (uuid/next)
|
||||||
|
out (th/command! {::th/type :create-team-with-invitations
|
||||||
|
::rpc/profile-id (:id profile)
|
||||||
|
:name "team with client id"
|
||||||
|
:emails ["invitee@example.com"]
|
||||||
|
:role :editor
|
||||||
|
:id sent-id})]
|
||||||
|
(t/is (th/ex-info? (:error out)))
|
||||||
|
(t/is (th/ex-of-type? (:error out) :validation))
|
||||||
|
(t/is (th/ex-of-code? (:error out) :params-validation))))
|
||||||
|
|||||||
@ -53,10 +53,13 @@
|
|||||||
(or (str/starts-with? url "http://")
|
(or (str/starts-with? url "http://")
|
||||||
(str/starts-with? url "https://"))))))
|
(str/starts-with? url "https://"))))))
|
||||||
(rx/mapcat (fn [item]
|
(rx/mapcat (fn [item]
|
||||||
|
;; The upload commands use closed params schemas, so
|
||||||
|
;; drop the svg-only attrs (:href :width :height) that
|
||||||
|
;; `svg/collect-images` attaches.
|
||||||
(->> (rp/cmd! (if (contains? item :content)
|
(->> (rp/cmd! (if (contains? item :content)
|
||||||
:upload-file-media-object
|
:upload-file-media-object
|
||||||
:create-file-media-object-from-url)
|
:create-file-media-object-from-url)
|
||||||
(dissoc item :href))
|
(dissoc item :href :width :height))
|
||||||
;; When the image uploaded fail we skip the shape
|
;; When the image uploaded fail we skip the shape
|
||||||
;; returning `nil` will afterward not create the shape.
|
;; returning `nil` will afterward not create the shape.
|
||||||
(rx/catch #(rx/of nil))
|
(rx/catch #(rx/of nil))
|
||||||
|
|||||||
63
frontend/test/frontend_tests/data/svg_upload_test.cljs
Normal file
63
frontend/test/frontend_tests/data/svg_upload_test.cljs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
;; 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.svg-upload-test
|
||||||
|
"Tests for the media upload triggered from an SVG import."
|
||||||
|
(:require
|
||||||
|
[app.common.transit :as tr]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
|
[app.main.data.workspace.svg-upload :as svg]
|
||||||
|
[beicon.v2.core :as rx]
|
||||||
|
[cljs.test :as t :include-macros true]
|
||||||
|
[frontend-tests.helpers.http :as http]))
|
||||||
|
|
||||||
|
(defn- image-node
|
||||||
|
[href]
|
||||||
|
{:tag :image
|
||||||
|
:attrs {:href href :width "10" :height "20"}
|
||||||
|
:content []})
|
||||||
|
|
||||||
|
(t/deftest upload-images-drops-svg-only-attrs
|
||||||
|
(t/testing "media upload params do not carry svg-only attrs"
|
||||||
|
(t/async done
|
||||||
|
(let [file-id (uuid/next)
|
||||||
|
bodies (atom [])
|
||||||
|
|
||||||
|
fetch-mock
|
||||||
|
(fn [url opts]
|
||||||
|
(swap! bodies conj {:cmd (http/url->cmd url)
|
||||||
|
:body (.-body opts)})
|
||||||
|
(js/Promise.resolve
|
||||||
|
(http/make-transit-response
|
||||||
|
{:id (uuid/next)
|
||||||
|
:file-id file-id
|
||||||
|
:name "pic"
|
||||||
|
:mtype "image/png"
|
||||||
|
:width 10
|
||||||
|
:height 20})))
|
||||||
|
|
||||||
|
orig (http/install-fetch-mock! fetch-mock)
|
||||||
|
svg-data {:tag :svg
|
||||||
|
:attrs {}
|
||||||
|
:content [(image-node "https://example.com/pic.png")]}]
|
||||||
|
|
||||||
|
(->> (svg/upload-images svg-data file-id)
|
||||||
|
(rx/subs!
|
||||||
|
(fn [_] nil)
|
||||||
|
(fn [err]
|
||||||
|
(http/restore-fetch! orig)
|
||||||
|
(t/is false (str "unexpected error: " (ex-message err)))
|
||||||
|
(done))
|
||||||
|
(fn []
|
||||||
|
(http/restore-fetch! orig)
|
||||||
|
(let [{:keys [cmd body]} (first @bodies)
|
||||||
|
params (tr/decode-str body)]
|
||||||
|
(t/is (= :create-file-media-object-from-url cmd))
|
||||||
|
(t/is (= "https://example.com/pic.png" (:url params)))
|
||||||
|
(t/is (not (contains? params :href)))
|
||||||
|
(t/is (not (contains? params :width)))
|
||||||
|
(t/is (not (contains? params :height))))
|
||||||
|
(done))))))))
|
||||||
@ -16,6 +16,7 @@
|
|||||||
[frontend-tests.data.profile-test]
|
[frontend-tests.data.profile-test]
|
||||||
[frontend-tests.data.repo-test]
|
[frontend-tests.data.repo-test]
|
||||||
[frontend-tests.data.store-test]
|
[frontend-tests.data.store-test]
|
||||||
|
[frontend-tests.data.svg-upload-test]
|
||||||
[frontend-tests.data.uploads-test]
|
[frontend-tests.data.uploads-test]
|
||||||
[frontend-tests.data.viewer-test]
|
[frontend-tests.data.viewer-test]
|
||||||
[frontend-tests.data.workspace-colors-test]
|
[frontend-tests.data.workspace-colors-test]
|
||||||
@ -134,6 +135,7 @@
|
|||||||
'frontend-tests.data.repo-test
|
'frontend-tests.data.repo-test
|
||||||
'frontend-tests.data.store-test
|
'frontend-tests.data.store-test
|
||||||
'frontend-tests.data.exports-assets-test
|
'frontend-tests.data.exports-assets-test
|
||||||
|
'frontend-tests.data.svg-upload-test
|
||||||
'frontend-tests.data.uploads-test
|
'frontend-tests.data.uploads-test
|
||||||
'frontend-tests.data.viewer-test
|
'frontend-tests.data.viewer-test
|
||||||
'frontend-tests.data.workspace-colors-test
|
'frontend-tests.data.workspace-colors-test
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user