mirror of
https://github.com/penpot/penpot.git
synced 2025-03-25 06:01:46 -05:00
🎉 Add usage quotes
This commit is contained in:
parent
941aa6ad5d
commit
73a3e0c0ae
20 changed files with 914 additions and 68 deletions
|
@ -102,7 +102,7 @@
|
|||
(s/def ::audit-log-archive-uri ::us/string)
|
||||
(s/def ::audit-log-http-handler-concurrency ::us/integer)
|
||||
|
||||
(s/def ::admins ::us/set-of-strings)
|
||||
(s/def ::admins ::us/set-of-valid-emails)
|
||||
(s/def ::file-change-snapshot-every ::us/integer)
|
||||
(s/def ::file-change-snapshot-timeout ::dt/duration)
|
||||
|
||||
|
@ -130,6 +130,14 @@
|
|||
(s/def ::database-min-pool-size ::us/integer)
|
||||
(s/def ::database-max-pool-size ::us/integer)
|
||||
|
||||
(s/def ::quotes-teams-per-profile ::us/integer)
|
||||
(s/def ::quotes-projects-per-team ::us/integer)
|
||||
(s/def ::quotes-invitations-per-team ::us/integer)
|
||||
(s/def ::quotes-profiles-per-team ::us/integer)
|
||||
(s/def ::quotes-files-per-project ::us/integer)
|
||||
(s/def ::quotes-files-per-team ::us/integer)
|
||||
(s/def ::quotes-font-variants-per-team ::us/integer)
|
||||
|
||||
(s/def ::default-blob-version ::us/integer)
|
||||
(s/def ::error-report-webhook ::us/string)
|
||||
(s/def ::user-feedback-destination ::us/string)
|
||||
|
@ -272,6 +280,15 @@
|
|||
::profile-complaint-max-age
|
||||
::profile-complaint-threshold
|
||||
::public-uri
|
||||
|
||||
::quotes-teams-per-profile
|
||||
::quotes-projects-per-team
|
||||
::quotes-invitations-per-team
|
||||
::quotes-profiles-per-team
|
||||
::quotes-files-per-project
|
||||
::quotes-files-per-team
|
||||
::quotes-font-variants-per-team
|
||||
|
||||
::redis-uri
|
||||
::registration-domain-whitelist
|
||||
::rpc-rlimit-config
|
||||
|
@ -311,7 +328,8 @@
|
|||
[:enable-backend-api-doc
|
||||
:enable-backend-worker
|
||||
:enable-secure-session-cookies
|
||||
:enable-email-verification])
|
||||
:enable-email-verification
|
||||
:enable-quotes])
|
||||
|
||||
(defn- parse-flags
|
||||
[config]
|
||||
|
|
|
@ -167,6 +167,7 @@
|
|||
(instance? javax.sql.DataSource v))
|
||||
|
||||
(s/def ::pool pool?)
|
||||
(s/def ::conn-or-pool some?)
|
||||
|
||||
(defn closed?
|
||||
[pool]
|
||||
|
|
|
@ -257,15 +257,17 @@
|
|||
"Schedule an already defined email to be sent using asynchronously
|
||||
using worker task."
|
||||
[{:keys [::conn ::factory] :as context}]
|
||||
(us/verify fn? factory)
|
||||
(us/verify some? conn)
|
||||
(let [email (factory context)]
|
||||
(wrk/submit! (assoc email
|
||||
::wrk/task :sendmail
|
||||
::wrk/delay 0
|
||||
::wrk/max-retries 4
|
||||
::wrk/priority 200
|
||||
::wrk/conn conn))))
|
||||
(let [email (if factory
|
||||
(factory context)
|
||||
(dissoc context ::conn))]
|
||||
(wrk/submit! (merge
|
||||
{::wrk/task :sendmail
|
||||
::wrk/delay 0
|
||||
::wrk/max-retries 4
|
||||
::wrk/priority 200
|
||||
::wrk/conn conn}
|
||||
email))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SENDMAIL FN / TASK HANDLER
|
||||
|
|
|
@ -302,7 +302,10 @@
|
|||
{:name "0097-mod-profile-table"
|
||||
:fn (mg/resource "app/migrations/sql/0097-mod-profile-table.sql")}
|
||||
|
||||
])
|
||||
{:name "0098-add-quotes-table"
|
||||
:fn (mg/resource "app/migrations/sql/0098-add-quotes-table.sql")}
|
||||
|
||||
])
|
||||
|
||||
|
||||
(defmethod ig/init-key ::migrations [_ _] migrations)
|
||||
|
|
82
backend/src/app/migrations/sql/0098-add-quotes-table.sql
Normal file
82
backend/src/app/migrations/sql/0098-add-quotes-table.sql
Normal file
|
@ -0,0 +1,82 @@
|
|||
CREATE TABLE usage_quote (
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||
target text NOT NULL,
|
||||
quote bigint NOT NULL,
|
||||
|
||||
profile_id uuid NULL REFERENCES profile(id) ON DELETE CASCADE DEFERRABLE,
|
||||
project_id uuid NULL REFERENCES project(id) ON DELETE CASCADE DEFERRABLE,
|
||||
team_id uuid NULL REFERENCES team(id) ON DELETE CASCADE DEFERRABLE,
|
||||
file_id uuid NULL REFERENCES file(id) ON DELETE CASCADE DEFERRABLE
|
||||
);
|
||||
|
||||
ALTER TABLE usage_quote
|
||||
ALTER COLUMN target SET STORAGE external;
|
||||
|
||||
CREATE INDEX usage_quote__profile_id__idx ON usage_quote(profile_id, target);
|
||||
CREATE INDEX usage_quote__project_id__idx ON usage_quote(project_id, target);
|
||||
CREATE INDEX usage_quote__team_id__idx ON usage_quote(team_id, target);
|
||||
|
||||
-- DROP TABLE IF EXISTS usage_quote_test;
|
||||
-- CREATE TABLE usage_quote_test (
|
||||
-- id bigserial NOT NULL PRIMARY KEY,
|
||||
-- target text NOT NULL,
|
||||
-- quote bigint NOT NULL,
|
||||
|
||||
-- profile_id bigint NULL,
|
||||
-- team_id bigint NULL,
|
||||
-- project_id bigint NULL,
|
||||
-- file_id bigint NULL
|
||||
-- );
|
||||
|
||||
-- ALTER TABLE usage_quote_test
|
||||
-- ALTER COLUMN target SET STORAGE external;
|
||||
|
||||
-- CREATE INDEX usage_quote_test__profile_id__idx ON usage_quote_test(profile_id, target);
|
||||
-- CREATE INDEX usage_quote_test__project_id__idx ON usage_quote_test(project_id, target);
|
||||
-- CREATE INDEX usage_quote_test__team_id__idx ON usage_quote_test(team_id, target);
|
||||
-- -- CREATE INDEX usage_quote_test__target__idx ON usage_quote_test(target);
|
||||
|
||||
-- DELETE FROM usage_quote_test;
|
||||
|
||||
-- INSERT INTO usage_quote_test (target, quote, profile_id, team_id, project_id)
|
||||
-- SELECT 'files-per-project', 50*RANDOM(), 2000*RANDOM(), null, null
|
||||
-- FROM generate_series(1, 5000);
|
||||
|
||||
-- INSERT INTO usage_quote_test (target, quote, profile_id, team_id, project_id)
|
||||
-- SELECT 'files-per-project', 200*RANDOM(), 300*RANDOM(), 300*RANDOM(), null
|
||||
-- FROM generate_series(1, 1000);
|
||||
|
||||
-- INSERT INTO usage_quote_test (target, quote, profile_id, team_id, project_id)
|
||||
-- SELECT 'files-per-project', 100*RANDOM(), 300*RANDOM(), null, 300*RANDOM()
|
||||
-- FROM generate_series(1, 1000);
|
||||
|
||||
-- INSERT INTO usage_quote_test (target, quote, profile_id, team_id, project_id)
|
||||
-- SELECT 'files-per-project', 100*RANDOM(), 300*RANDOM(), 300*RANDOM(), 300*RANDOM()
|
||||
-- FROM generate_series(1, 1000);
|
||||
|
||||
-- INSERT INTO usage_quote_test (target, quote, profile_id, team_id, project_id)
|
||||
-- SELECT 'files-per-project', 30*RANDOM(), null, 2000*RANDOM(), null
|
||||
-- FROM generate_series(1, 5000);
|
||||
|
||||
-- INSERT INTO usage_quote_test (target, quote, profile_id, team_id, project_id)
|
||||
-- SELECT 'files-per-project', 10*RANDOM(), null, null, 2000*RANDOM()
|
||||
-- FROM generate_series(1, 5000);
|
||||
|
||||
-- VACUUM ANALYZE usage_quote_test;
|
||||
|
||||
-- select * from usage_quote_test
|
||||
-- where target = 'files-per-project'
|
||||
-- and profile_id = 1
|
||||
-- and team_id is null
|
||||
-- and project_id is null;
|
||||
|
||||
-- select * from usage_quote_test
|
||||
-- where target = 'files-per-project'
|
||||
-- and ((team_id = 1 and (profile_id = 1 or profile_id is null)) or
|
||||
-- (profile_id = 1 and team_id is null and project_id is null));
|
||||
|
||||
-- select * from usage_quote_test
|
||||
-- where target = 'files-per-project'
|
||||
-- and ((project_id = 1 and (profile_id = 1 or profile_id is null)) or
|
||||
-- (team_id = 1 and (profile_id = 1 or profile_id is null)) or
|
||||
-- (profile_id = 1 and team_id is null and project_id is null));
|
|
@ -18,6 +18,7 @@
|
|||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.permissions :as perms]
|
||||
[app.rpc.queries.projects :as proj]
|
||||
[app.rpc.quotes :as quotes]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.objects-map :as omap]
|
||||
[app.util.pointer-map :as pmap]
|
||||
|
@ -84,6 +85,12 @@
|
|||
(proj/check-edition-permissions! conn profile-id project-id)
|
||||
(let [team-id (files/get-team-id conn project-id)
|
||||
params (assoc params :profile-id profile-id)]
|
||||
|
||||
(run! (partial quotes/check-quote! conn)
|
||||
(list {::quotes/id ::quotes/files-per-project
|
||||
::quotes/team-id team-id
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/project-id project-id}))
|
||||
|
||||
(-> (create-file conn params)
|
||||
(vary-meta assoc ::audit/props {:team-id team-id})))))
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
[app.rpc.helpers :as rph]
|
||||
[app.rpc.permissions :as perms]
|
||||
[app.rpc.queries.profile :as profile]
|
||||
[app.rpc.quotes :as quotes]
|
||||
[app.storage :as sto]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.services :as sv]
|
||||
|
@ -297,6 +298,9 @@
|
|||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(quotes/check-quote! conn {::quotes/id ::quotes/teams-per-profile
|
||||
::quotes/profile-id profile-id})
|
||||
|
||||
(create-team conn (assoc params :profile-id profile-id))))
|
||||
|
||||
(defn create-team
|
||||
|
@ -739,6 +743,17 @@
|
|||
team (db/get-by-id conn :team team-id)
|
||||
emails (cond-> (or emails #{}) (string? email) (conj email))]
|
||||
|
||||
|
||||
(run! (partial quotes/check-quote! conn)
|
||||
(list {::quotes/id ::quotes/invitations-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id (:id team)
|
||||
::quotes/incr (count emails)}
|
||||
{::quotes/id ::quotes/profiles-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id (:id team)
|
||||
::quotes/incr (count emails)}))
|
||||
|
||||
(when-not (:is-admin perms)
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
|
@ -785,6 +800,18 @@
|
|||
:role role}))
|
||||
(run! (partial create-invitation cfg)))
|
||||
|
||||
(run! (partial quotes/check-quote! conn)
|
||||
(list {::quotes/id ::quotes/teams-per-profile
|
||||
::quotes/profile-id profile-id}
|
||||
{::quotes/id ::quotes/invitations-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id (:id team)
|
||||
::quotes/incr (count emails)}
|
||||
{::quotes/id ::quotes/profiles-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id (:id team)
|
||||
::quotes/incr (count emails)}))
|
||||
|
||||
(-> team
|
||||
(vary-meta assoc ::audit/props {:invitations (count emails)})
|
||||
(rph/with-defer
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.rpc.queries.profile :as profile]
|
||||
[app.rpc.quotes :as quotes]
|
||||
[app.tokens :as tokens]
|
||||
[app.tokens.spec.team-invitation :as-alias spec.team-invitation]
|
||||
[app.util.services :as sv]
|
||||
|
@ -96,6 +97,11 @@
|
|||
(ex/raise :type :restriction
|
||||
:code :profile-blocked))
|
||||
|
||||
(quotes/check-quote! conn
|
||||
{::quotes/id ::quotes/profiles-per-team
|
||||
::quotes/profile-id (:id member)
|
||||
::quotes/team-id team-id})
|
||||
|
||||
;; Insert the invited member to the team
|
||||
(db/insert! conn :team-profile-rel params {:on-conflict-do-nothing true})
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.rpc.quotes :as quotes]
|
||||
[app.storage :as sto]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
|
@ -49,6 +50,9 @@
|
|||
[{:keys [pool] :as cfg} {:keys [team-id profile-id] :as params}]
|
||||
(let [cfg (update cfg :storage media/configure-assets-storage)]
|
||||
(teams/check-edition-permissions! pool profile-id team-id)
|
||||
(quotes/check-quote! pool {::quotes/id ::quotes/font-variants-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id})
|
||||
(create-font-variant cfg params)))
|
||||
|
||||
(defn create-font-variant
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.rpc.queries.projects :as proj]
|
||||
[app.rpc.quotes :as quotes]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
@ -37,6 +38,10 @@
|
|||
[{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(teams/check-edition-permissions! conn profile-id team-id)
|
||||
(quotes/check-quote! conn {::quotes/id ::quotes/projects-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id})
|
||||
|
||||
(let [project (teams/create-project conn params)]
|
||||
(teams/create-project-role conn profile-id (:id project) :owner)
|
||||
|
||||
|
|
327
backend/src/app/rpc/quotes.clj
Normal file
327
backend/src/app/rpc/quotes.clj
Normal file
|
@ -0,0 +1,327 @@
|
|||
;; 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 app.rpc.quotes
|
||||
"Penpot resource usage quotes."
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defmulti check-quote ::id)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; PUBLIC API
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(s/def ::conn ::db/conn-or-pool)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::project-id ::us/uuid)
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::incr (s/and int? pos?))
|
||||
(s/def ::target ::us/string)
|
||||
|
||||
(s/def ::quote
|
||||
(s/keys :req [::id ::profile-id]
|
||||
:opt [::conn
|
||||
::team-id
|
||||
::project-id
|
||||
::file-id
|
||||
::incr]))
|
||||
|
||||
(def ^:private enabled (volatile! true))
|
||||
|
||||
(defn enable!
|
||||
"Enable quotes checking at runtime (from server REPL)."
|
||||
[]
|
||||
(vswap! enabled (constantly true)))
|
||||
|
||||
(defn disable!
|
||||
"Disable quotes checking at runtime (from server REPL)."
|
||||
[]
|
||||
(vswap! enabled (constantly false)))
|
||||
|
||||
(defn check-quote!
|
||||
[conn quote]
|
||||
(us/assert! ::db/conn-or-pool conn)
|
||||
(us/assert! ::quote quote)
|
||||
(when (contains? cf/flags :quotes)
|
||||
(when @enabled
|
||||
(check-quote (assoc quote ::conn conn ::target (name (::id quote)))))))
|
||||
|
||||
(defn- send-notification!
|
||||
[{:keys [::conn] :as params}]
|
||||
(when-let [admins (seq (cf/get :admins))]
|
||||
(let [subject (str/istr "[quotes:notification]: max quote reached ~(::target params)")
|
||||
content (str/istr "- Param: profile-id '~(::profile-id params)}'\n"
|
||||
"- Param: team-id '~(::team-id params)'\n"
|
||||
"- Param: project-id '~(::project-id params)'\n"
|
||||
"- Param: file-id '~(::file-id params)'\n"
|
||||
"- Quote ID: '~(::target params)'\n"
|
||||
"- Max: ~(::quote params)\n"
|
||||
"- Total: ~(::total params) (INCR ~(::incr params 1))\n")]
|
||||
(wrk/submit! {::wrk/task :sendmail
|
||||
::wrk/delay (dt/duration "30s")
|
||||
::wrk/max-retries 4
|
||||
::wrk/priority 200
|
||||
::wrk/conn conn
|
||||
::wrk/dedupe true
|
||||
::wrk/label "quotes-notification"
|
||||
:to (vec admins)
|
||||
:subject subject
|
||||
:body [{:type "text/plain"
|
||||
:content content}]}))))
|
||||
|
||||
(defn- generic-check!
|
||||
[{:keys [::conn ::incr ::quote-sql ::count-sql ::default ::target] :or {incr 1} :as params}]
|
||||
(let [quote (->> (db/exec! conn quote-sql)
|
||||
(map :quote)
|
||||
(reduce max (- Integer/MAX_VALUE)))
|
||||
quote (if (pos? quote) quote default)
|
||||
total (->> (db/exec! conn count-sql) first :total)]
|
||||
|
||||
(when (> (+ total incr) quote)
|
||||
(if (contains? cf/flags :soft-quotes)
|
||||
(send-notification! (assoc params ::quote quote ::total total))
|
||||
(ex/raise :type :restriction
|
||||
:code :max-quote-reached
|
||||
:target target
|
||||
:quote quote
|
||||
:count total)))))
|
||||
|
||||
(def ^:private sql:get-quotes-1
|
||||
"select id, quote from usage_quote
|
||||
where target = ?
|
||||
and profile_id = ?
|
||||
and team_id is null
|
||||
and project_id is null
|
||||
and file_id is null;")
|
||||
|
||||
(def ^:private sql:get-quotes-2
|
||||
"select id, quote from usage_quote
|
||||
where target = ?
|
||||
and ((team_id = ? and (profile_id = ? or profile_id is null)) or
|
||||
(profile_id = ? and team_id is null and project_id is null and file_id is null));")
|
||||
|
||||
(def ^:private sql:get-quotes-3
|
||||
"select id, quote from usage_quote
|
||||
where target = ?
|
||||
and ((project_id = ? and (profile_id = ? or profile_id is null)) or
|
||||
(team_id = ? and (profile_id = ? or profile_id is null)) or
|
||||
(profile_id = ? and team_id is null and project_id is null and file_id is null));")
|
||||
|
||||
(def ^:private sql:get-quotes-4
|
||||
"select id, quote from usage_quote
|
||||
where target = ?
|
||||
and ((file_id = ? and (profile_id = ? or profile_id is null)) or
|
||||
(project_id = ? and (profile_id = ? or profile_id is null)) or
|
||||
(team_id = ? and (profile_id = ? or profile_id is null)) or
|
||||
(profile_id = ? and team_id is null and project_id is null and file_id is null));")
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: TEAMS-PER-PROFILE
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:get-teams-per-profile
|
||||
"select count(*) as total
|
||||
from team_profile_rel
|
||||
where profile_id = ?")
|
||||
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::teams-per-profile
|
||||
(s/keys :req [::profile-id ::target]))
|
||||
|
||||
(defmethod check-quote ::teams-per-profile
|
||||
[{:keys [::profile-id ::target] :as quote}]
|
||||
(us/assert! ::teams-per-profile quote)
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-teams-per-profile Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-1 target profile-id])
|
||||
(assoc ::count-sql [sql:get-teams-per-profile profile-id])
|
||||
(generic-check!)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: PROJECTS-PER-TEAM
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:get-projects-per-team
|
||||
"select count(*) as total
|
||||
from project as p
|
||||
where p.team_id = ?
|
||||
and p.deleted_at is null")
|
||||
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::projects-per-team
|
||||
(s/keys :req [::profile-id ::team-id ::target]))
|
||||
|
||||
(defmethod check-quote ::projects-per-team
|
||||
[{:keys [::profile-id ::team-id ::target] :as quote}]
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-projects-per-team Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
|
||||
(assoc ::count-sql [sql:get-projects-per-team team-id])
|
||||
(generic-check!)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: FONT-VARIANTS-PER-TEAM
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:get-font-variants-per-team
|
||||
"select count(*) as total
|
||||
from team_font_variant as v
|
||||
where v.team_id = ?")
|
||||
|
||||
(s/def ::font-variants-per-team
|
||||
(s/keys :req [::profile-id ::team-id ::target]))
|
||||
|
||||
(defmethod check-quote ::font-variants-per-team
|
||||
[{:keys [::profile-id ::team-id ::target] :as quote}]
|
||||
(us/assert! ::font-variants-per-team quote)
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-font-variants-per-team Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
|
||||
(assoc ::count-sql [sql:get-font-variants-per-team team-id])
|
||||
(generic-check!)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: INVITATIONS-PER-TEAM
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:get-invitations-per-team
|
||||
"select count(*) as total
|
||||
from team_invitation
|
||||
where team_id = ?")
|
||||
|
||||
(s/def ::invitations-per-team
|
||||
(s/keys :req [::profile-id ::team-id ::target]))
|
||||
|
||||
(defmethod check-quote ::invitations-per-team
|
||||
[{:keys [::profile-id ::team-id ::target] :as quote}]
|
||||
(us/assert! ::invitations-per-team quote)
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-invitations-per-team Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
|
||||
(assoc ::count-sql [sql:get-invitations-per-team team-id])
|
||||
(generic-check!)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: PROFILES-PER-TEAM
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:get-profiles-per-team
|
||||
"select (select count(*)
|
||||
from team_profile_rel
|
||||
where team_id = ?) +
|
||||
(select count(*)
|
||||
from team_invitation
|
||||
where team_id = ?
|
||||
and valid_until > now()) as total;")
|
||||
|
||||
;; NOTE: the total number of profiles is determined by the number of
|
||||
;; effective members plus ongoing valid invitations.
|
||||
|
||||
(s/def ::profiles-per-team
|
||||
(s/keys :req [::profile-id ::team-id ::target]))
|
||||
|
||||
(defmethod check-quote ::profiles-per-team
|
||||
[{:keys [::profile-id ::team-id ::target] :as quote}]
|
||||
(us/assert! ::profiles-per-team quote)
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-profiles-per-team Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
|
||||
(assoc ::count-sql [sql:get-profiles-per-team team-id team-id])
|
||||
(generic-check!)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: FILES-PER-PROJECT
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:get-files-per-project
|
||||
"select count(*) as total
|
||||
from file as f
|
||||
where f.project_id = ?
|
||||
and f.deleted_at is null")
|
||||
|
||||
(s/def ::project-id ::us/uuid)
|
||||
(s/def ::files-per-project
|
||||
(s/keys :req [::profile-id ::project-id ::team-id ::target]))
|
||||
|
||||
(defmethod check-quote ::files-per-project
|
||||
[{:keys [::profile-id ::project-id ::team-id ::target] :as quote}]
|
||||
(us/assert! ::files-per-project quote)
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-files-per-project Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-3 target project-id profile-id team-id profile-id profile-id])
|
||||
(assoc ::count-sql [sql:get-files-per-project project-id])
|
||||
(generic-check!)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: COMMENT-THREADS-PER-FILE
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:get-comment-threads-per-file
|
||||
"select count(*) as total
|
||||
from comment_thread as ct
|
||||
where ct.file_id = ?")
|
||||
|
||||
(s/def ::comment-threads-per-file
|
||||
(s/keys :req [::profile-id ::project-id ::team-id ::target]))
|
||||
|
||||
(defmethod check-quote ::comment-threads-per-file
|
||||
[{:keys [::profile-id ::file-id ::team-id ::project-id ::target] :as quote}]
|
||||
(us/assert! ::files-per-project quote)
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-comment-threads-per-file Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-4 target project-id profile-id team-id profile-id profile-id])
|
||||
(assoc ::count-sql [sql:get-comment-threads-per-file file-id])
|
||||
(generic-check!)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: COMMENTS-PER-FILE
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:get-comments-per-file
|
||||
"select count(*) as total
|
||||
from comment as c
|
||||
join comment_thread as ct on (ct.id = c.thread_id)
|
||||
where ct.file_id = ?")
|
||||
|
||||
(s/def ::comments-per-file
|
||||
(s/keys :req [::profile-id ::project-id ::team-id ::target]))
|
||||
|
||||
(defmethod check-quote ::comments-per-file
|
||||
[{:keys [::profile-id ::file-id ::team-id ::project-id ::target] :as quote}]
|
||||
(us/assert! ::files-per-project quote)
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-comments-per-file Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-4 target file-id profile-id project-id
|
||||
profile-id team-id profile-id profile-id])
|
||||
(assoc ::count-sql [sql:get-comments-per-file file-id])
|
||||
(generic-check!)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: DEFAULT
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defmethod check-quote :default
|
||||
[{:keys [::id]}]
|
||||
(ex/raise :type :internal
|
||||
:code :quote-not-defined
|
||||
:quote id
|
||||
:hint "backend using a quote identifier not defined"))
|
|
@ -59,7 +59,8 @@
|
|||
(def default-flags
|
||||
[:enable-secure-session-cookies
|
||||
:enable-email-verification
|
||||
:enable-smtp])
|
||||
:enable-smtp
|
||||
:enable-quotes])
|
||||
|
||||
(defn state-init
|
||||
[next]
|
||||
|
|
|
@ -6,52 +6,56 @@
|
|||
|
||||
(ns backend-tests.rpc-font-test
|
||||
(:require
|
||||
[backend-tests.helpers :as th]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.http :as http]
|
||||
[app.storage :as sto]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
[datoteka.fs :as fs]
|
||||
[datoteka.io :as io]))
|
||||
[datoteka.io :as io]
|
||||
[mockery.core :refer [with-mocks]]))
|
||||
|
||||
(t/use-fixtures :once th/state-init)
|
||||
(t/use-fixtures :each th/database-reset)
|
||||
|
||||
(t/deftest ttf-font-upload-1
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
team-id (:default-team-id prof)
|
||||
proj-id (:default-project-id prof)
|
||||
font-id (uuid/custom 10 1)
|
||||
(with-mocks [mock {:target 'app.rpc.quotes/check-quote! :return nil}]
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
team-id (:default-team-id prof)
|
||||
proj-id (:default-project-id prof)
|
||||
font-id (uuid/custom 10 1)
|
||||
|
||||
ttfdata (-> (io/resource "backend_tests/test_files/font-1.ttf")
|
||||
io/input-stream
|
||||
io/read-as-bytes)
|
||||
ttfdata (-> (io/resource "backend_tests/test_files/font-1.ttf")
|
||||
io/input-stream
|
||||
io/read-as-bytes)
|
||||
|
||||
params {::th/type :create-font-variant
|
||||
:profile-id (:id prof)
|
||||
:team-id team-id
|
||||
:font-id font-id
|
||||
:font-family "somefont"
|
||||
:font-weight 400
|
||||
:font-style "normal"
|
||||
:data {"font/ttf" ttfdata}}
|
||||
out (th/mutation! params)]
|
||||
params {::th/type :create-font-variant
|
||||
:profile-id (:id prof)
|
||||
:team-id team-id
|
||||
:font-id font-id
|
||||
:font-family "somefont"
|
||||
:font-weight 400
|
||||
:font-style "normal"
|
||||
:data {"font/ttf" ttfdata}}
|
||||
out (th/mutation! params)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(let [result (:result out)]
|
||||
(t/is (uuid? (:id result)))
|
||||
(t/is (uuid? (:ttf-file-id result)))
|
||||
(t/is (uuid? (:otf-file-id result)))
|
||||
(t/is (uuid? (:woff1-file-id result)))
|
||||
(t/are [k] (= (get params k)
|
||||
(get result k))
|
||||
:team-id
|
||||
:font-id
|
||||
:font-family
|
||||
:font-weight
|
||||
:font-style))))
|
||||
(t/is (= 1 (:call-count @mock)))
|
||||
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(let [result (:result out)]
|
||||
(t/is (uuid? (:id result)))
|
||||
(t/is (uuid? (:ttf-file-id result)))
|
||||
(t/is (uuid? (:otf-file-id result)))
|
||||
(t/is (uuid? (:woff1-file-id result)))
|
||||
(t/are [k] (= (get params k)
|
||||
(get result k))
|
||||
:team-id
|
||||
:font-id
|
||||
:font-family
|
||||
:font-weight
|
||||
:font-style)))))
|
||||
|
||||
(t/deftest ttf-font-upload-2
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
|
|
344
backend/test/backend_tests/rpc_quotes_test.clj
Normal file
344
backend/test/backend_tests/rpc_quotes_test.clj
Normal file
|
@ -0,0 +1,344 @@
|
|||
;; 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 backend-tests.rpc-quotes-test
|
||||
(:require
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.http :as http]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.cond :as cond]
|
||||
[app.rpc.quotes :as-alias quotes]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
[datoteka.core :as fs]
|
||||
[mockery.core :refer [with-mocks]]))
|
||||
|
||||
(t/use-fixtures :once th/state-init)
|
||||
(t/use-fixtures :each th/database-reset)
|
||||
|
||||
(t/deftest teams-per-profile-quote
|
||||
(with-mocks [mock {:target 'app.config/get
|
||||
:return (th/config-get-mock
|
||||
{:quotes-teams-per-profile 2})}]
|
||||
|
||||
(let [profile-1 (th/create-profile* 1)
|
||||
profile-2 (th/create-profile* 2)
|
||||
data {::th/type :create-team
|
||||
::rpc/profile-id (:id profile-1)}
|
||||
check-ok! (fn [n]
|
||||
(let [data (assoc data :name (str "team" n))
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (some? (:result out)))))
|
||||
check-ko! (fn [n]
|
||||
(let [data (assoc data :name (str "team" n))
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (not (th/success? out)))
|
||||
(let [error (:error out)]
|
||||
(t/is (= :restriction (th/ex-type error)))
|
||||
(t/is (= :max-quote-reached (th/ex-code error)))
|
||||
(t/is (= "teams-per-profile" (:target (ex-data error)))))))]
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:profile-id (:id profile-2)
|
||||
:target "teams-per-profile"
|
||||
:quote 100})
|
||||
|
||||
(check-ok! 1)
|
||||
(check-ko! 2)
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:profile-id (:id profile-1)
|
||||
:target "teams-per-profile"
|
||||
:quote 3})
|
||||
|
||||
(check-ok! 2)
|
||||
(check-ko! 3))))
|
||||
|
||||
(t/deftest projects-per-team-quote
|
||||
(with-mocks [mock {:target 'app.config/get
|
||||
:return (th/config-get-mock
|
||||
{:quotes-projects-per-team 2})}]
|
||||
|
||||
(let [profile-1 (th/create-profile* 1)
|
||||
profile-2 (th/create-profile* 2)
|
||||
team-id (:default-team-id profile-1)
|
||||
data {::th/type :create-project
|
||||
:profile-id (:id profile-1)
|
||||
:team-id team-id}
|
||||
|
||||
check-ok! (fn [name]
|
||||
(let [data (assoc data :name (str "project" name))
|
||||
out (th/mutation! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (some? (:result out)))))
|
||||
|
||||
check-ko! (fn [name]
|
||||
;; create second project
|
||||
(let [data (assoc data :name (str "project" name))
|
||||
out (th/mutation! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (not (th/success? out)))
|
||||
(let [error (:error out)]
|
||||
(t/is (= :restriction (th/ex-type error)))
|
||||
(t/is (= :max-quote-reached (th/ex-code error)))
|
||||
(t/is (= "projects-per-team" (:target (ex-data error)))))))]
|
||||
|
||||
(check-ok! 1)
|
||||
(check-ko! 2)
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:team-id team-id
|
||||
:target "projects-per-team"
|
||||
:quote 3})
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:team-id team-id
|
||||
:profile-id (:id profile-2)
|
||||
:target "projects-per-team"
|
||||
:quote 10})
|
||||
|
||||
(check-ok! 2)
|
||||
(check-ko! 3)
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:team-id team-id
|
||||
:profile-id (:id profile-1)
|
||||
:target "projects-per-team"
|
||||
:quote 4})
|
||||
|
||||
(check-ok! 3)
|
||||
(check-ko! 4)
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:profile-id (:id profile-1)
|
||||
:target "projects-per-team"
|
||||
:quote 5})
|
||||
|
||||
(check-ok! 4)
|
||||
(check-ko! 5)
|
||||
|
||||
)))
|
||||
|
||||
(t/deftest invitations-per-team-quote
|
||||
(with-mocks [mock {:target 'app.config/get
|
||||
:return (th/config-get-mock
|
||||
{:quotes-invitations-per-team 2})}]
|
||||
(let [profile-1 (th/create-profile* 1)
|
||||
profile-2 (th/create-profile* 2)
|
||||
data {::th/type :create-team-invitations
|
||||
::rpc/profile-id (:id profile-1)
|
||||
:team-id (:default-team-id profile-1)
|
||||
:role :editor}
|
||||
|
||||
check-ok! (fn [n]
|
||||
(let [data (assoc data :emails [(str "foo" n "@example.net")])
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (some? (:result out)))))
|
||||
check-ko! (fn [n]
|
||||
(let [data (assoc data :emails [(str "foo" n "@example.net")])
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (not (th/success? out)))
|
||||
(let [error (:error out)]
|
||||
(t/is (= :restriction (th/ex-type error)))
|
||||
(t/is (= :max-quote-reached (th/ex-code error)))
|
||||
(t/is (= "invitations-per-team" (:target (ex-data error)))))))]
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:profile-id (:id profile-2)
|
||||
:target "invitations-per-team"
|
||||
:quote 100})
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:team-id (:default-team-id profile-2)
|
||||
:target "invitations-per-team"
|
||||
:quote 100})
|
||||
|
||||
(check-ok! 1)
|
||||
(check-ok! 2)
|
||||
(check-ko! 3)
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:team-id (:default-team-id profile-1)
|
||||
:target "invitations-per-team"
|
||||
:quote 3})
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:team-id (:default-team-id profile-1)
|
||||
:profile-id (:id profile-2)
|
||||
:target "invitations-per-team"
|
||||
:quote 100})
|
||||
|
||||
(check-ok! 3)
|
||||
(check-ko! 4)
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:team-id (:default-team-id profile-1)
|
||||
:profile-id (:id profile-1)
|
||||
:target "invitations-per-team"
|
||||
:quote 4})
|
||||
|
||||
(check-ok! 4)
|
||||
(check-ko! 5)
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:profile-id (:id profile-1)
|
||||
:target "invitations-per-team"
|
||||
:quote 5})
|
||||
|
||||
(check-ok! 5)
|
||||
(check-ko! 6))))
|
||||
|
||||
|
||||
(t/deftest profiles-per-team-quote
|
||||
(with-mocks [mock {:target 'app.config/get
|
||||
:return (th/config-get-mock
|
||||
{:quotes-profiles-per-team 3})}]
|
||||
(let [profile-1 (th/create-profile* 1)
|
||||
profile-2 (th/create-profile* 2)
|
||||
data {::th/type :create-team-invitations
|
||||
::rpc/profile-id (:id profile-1)
|
||||
:team-id (:default-team-id profile-1)
|
||||
:role :editor}
|
||||
|
||||
check-ok! (fn [n]
|
||||
(let [data (assoc data :emails [(str "foo" n "@example.net")])
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (some? (:result out)))))
|
||||
check-ko! (fn [n]
|
||||
(let [data (assoc data :emails [(str "foo" n "@example.net")])
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (not (th/success? out)))
|
||||
(let [error (:error out)]
|
||||
(t/is (= :restriction (th/ex-type error)))
|
||||
(t/is (= :max-quote-reached (th/ex-code error)))
|
||||
(t/is (= "profiles-per-team" (:target (ex-data error)))))))]
|
||||
|
||||
(th/create-team-role* {:team-id (:default-team-id profile-1)
|
||||
:profile-id (:id profile-2)
|
||||
:role :admin})
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:profile-id (:id profile-2)
|
||||
:target "profiles-per-team"
|
||||
:quote 100})
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:team-id (:default-team-id profile-2)
|
||||
:target "profiles-per-team"
|
||||
:quote 100})
|
||||
|
||||
|
||||
(check-ok! 1)
|
||||
(check-ko! 2)
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:team-id (:default-team-id profile-1)
|
||||
:target "profiles-per-team"
|
||||
:quote 4})
|
||||
|
||||
(check-ok! 2)
|
||||
(check-ko! 3))))
|
||||
|
||||
|
||||
|
||||
(t/deftest files-per-project-quote
|
||||
(with-mocks [mock {:target 'app.config/get
|
||||
:return (th/config-get-mock
|
||||
{:quotes-files-per-project 1})}]
|
||||
|
||||
(let [profile-1 (th/create-profile* 1)
|
||||
profile-2 (th/create-profile* 2)
|
||||
project-1 (th/create-project* 1 {:profile-id (:id profile-1)
|
||||
:team-id (:default-team-id profile-1)})
|
||||
project-2 (th/create-project* 2 {:profile-id (:id profile-2)
|
||||
:team-id (:default-team-id profile-2)})
|
||||
data {::th/type :create-file
|
||||
::rpc/profile-id (:id profile-1)
|
||||
:project-id (:id project-1)}
|
||||
check-ok! (fn [n]
|
||||
(let [data (assoc data :name (str "file" n))
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (some? (:result out)))))
|
||||
check-ko! (fn [n]
|
||||
(let [data (assoc data :name (str "file" n))
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (not (th/success? out)))
|
||||
(let [error (:error out)]
|
||||
(t/is (= :restriction (th/ex-type error)))
|
||||
(t/is (= :max-quote-reached (th/ex-code error)))
|
||||
(t/is (= "files-per-project" (:target (ex-data error)))))))]
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:project-id (:id project-2)
|
||||
:target "files-per-project"
|
||||
:quote 100})
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:team-id (:team-id project-2)
|
||||
:target "files-per-project"
|
||||
:quote 100})
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:profile-id (:id profile-2)
|
||||
:target "files-per-project"
|
||||
:quote 100})
|
||||
|
||||
|
||||
(check-ok! 1)
|
||||
(check-ko! 2)
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:project-id (:id project-1)
|
||||
:target "files-per-project"
|
||||
:quote 2})
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:project-id (:id project-1)
|
||||
:profile-id (:id profile-2)
|
||||
:target "files-per-project"
|
||||
:quote 100})
|
||||
|
||||
(check-ok! 2)
|
||||
(check-ko! 3)
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:team-id (:team-id project-1)
|
||||
:target "files-per-project"
|
||||
:quote 3})
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:team-id (:team-id project-1)
|
||||
:profile-id (:id profile-2)
|
||||
:target "files-per-project"
|
||||
:quote 100})
|
||||
|
||||
|
||||
(check-ok! 3)
|
||||
(check-ko! 4)
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:profile-id (:id profile-1)
|
||||
:target "files-per-project"
|
||||
:quote 4})
|
||||
|
||||
(check-ok! 4)
|
||||
(check-ko! 5)
|
||||
|
||||
)))
|
|
@ -249,14 +249,6 @@
|
|||
(s/with-gen (s/and string? #(not (str/empty? %)))
|
||||
#(tgen/such-that (complement str/empty?) (s/gen ::string))))
|
||||
|
||||
(s/def ::url ::string)
|
||||
(s/def ::fn fn?)
|
||||
(s/def ::id ::uuid)
|
||||
|
||||
(s/def ::set-of-string (s/every ::string :kind set?))
|
||||
(s/def ::coll-of-uuid (s/every ::uuid))
|
||||
(s/def ::set-of-uuid (s/every ::uuid :kind set?))
|
||||
|
||||
#?(:clj
|
||||
(s/def ::agent #(instance? clojure.lang.Agent %)))
|
||||
|
||||
|
@ -300,6 +292,13 @@
|
|||
(s/with-gen safe-number? #(tgen/one-of [(s/gen ::safe-integer)
|
||||
(s/gen ::safe-float)])))
|
||||
|
||||
(s/def ::url ::string)
|
||||
(s/def ::fn fn?)
|
||||
(s/def ::id ::uuid)
|
||||
(s/def ::some some?)
|
||||
(s/def ::coll-of-uuid (s/every ::uuid))
|
||||
(s/def ::set-of-uuid (s/every ::uuid :kind set?))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; MACROS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
|
|
@ -247,7 +247,7 @@
|
|||
(defn top-nested-frame-ids
|
||||
"Search the top nested frame in a list of ids"
|
||||
[objects ids]
|
||||
|
||||
|
||||
(let [frame-ids (->> ids (filter #(cph/frame-shape? objects %)))
|
||||
frame-set (set frame-ids)]
|
||||
(loop [current-id (first frame-ids)]
|
||||
|
@ -296,11 +296,14 @@
|
|||
[p1 (+ 1 (d/parse-integer p2))]
|
||||
[basename 1]))
|
||||
|
||||
(s/def ::set-of-strings
|
||||
(s/every ::us/string :kind set?))
|
||||
|
||||
(defn generate-unique-name
|
||||
"A unique name generator"
|
||||
[used basename]
|
||||
(s/assert ::us/set-of-string used)
|
||||
(s/assert ::us/string basename)
|
||||
(us/assert! ::set-of-strings used)
|
||||
(us/assert! ::us/string basename)
|
||||
(if-not (contains? used basename)
|
||||
basename
|
||||
(let [[prefix initial] (extract-numeric-suffix basename)]
|
||||
|
@ -355,8 +358,8 @@
|
|||
[new-object new-objects updated-objects])
|
||||
|
||||
(let [child-id (first child-ids)
|
||||
child (get objects child-id)
|
||||
_ (us/assert some? child)
|
||||
child (get objects child-id)
|
||||
_ (us/assert! ::us/some child)
|
||||
|
||||
[new-child new-child-objects updated-child-objects]
|
||||
(clone-object child new-id objects update-new-object update-original-object)]
|
||||
|
|
|
@ -189,7 +189,6 @@
|
|||
(s/def ::file-change-event
|
||||
(s/keys :req-un [::type ::profile-id ::file-id ::session-id ::revn ::changes]))
|
||||
|
||||
|
||||
(defn handle-file-change
|
||||
[{:keys [file-id changes] :as msg}]
|
||||
(us/assert ::file-change-event msg)
|
||||
|
|
|
@ -173,20 +173,18 @@
|
|||
(cond
|
||||
(= :feature-mismatch code)
|
||||
(let [message (tr "errors.feature-mismatch" (:feature error))]
|
||||
(st/emit! (modal/show
|
||||
{:type :alert
|
||||
:message message
|
||||
:on-accept #(prn "kaka")})))
|
||||
(st/emit! (modal/show {:type :alert :message message})))
|
||||
|
||||
(= :features-not-supported code)
|
||||
(let [message (tr "errors.feature-not-supported" (:feature error))]
|
||||
(st/emit! (modal/show
|
||||
{:type :alert
|
||||
:message message
|
||||
:on-accept #(prn "kaka")})))
|
||||
(st/emit! (modal/show {:type :alert :message message})))
|
||||
|
||||
(= :max-quote-reached code)
|
||||
(let [message (tr "errors.max-quote-reached" (:target error))]
|
||||
(st/emit! (modal/show {:type :alert :message message})))
|
||||
|
||||
:else
|
||||
(ptk/handle-error (assoc error :type :server-error))))
|
||||
(ptk/handle-error {:type :server-error :data error})))
|
||||
|
||||
;; This happens when the backed server fails to process the
|
||||
;; request. This can be caused by an internal assertion or any other
|
||||
|
|
|
@ -765,6 +765,10 @@ msgstr "Your browser cannot do this operation"
|
|||
msgid "errors.feature-not-supported"
|
||||
msgstr "Feature '%s' is not supported."
|
||||
|
||||
#: src/app/main/errors.cljs
|
||||
msgid "errors.max-quote-reached"
|
||||
msgstr "You have reached the '%s' quote. Contact with support."
|
||||
|
||||
#: src/app/main/errors.cljs
|
||||
msgid "errors.feature-mismatch"
|
||||
msgstr "Looks like you are opening a file that has the feature '%s' enabled bug your penpot frontend does not supports it or has it disabled."
|
||||
|
|
|
@ -743,6 +743,18 @@ msgstr "Webhook modificado con éxito"
|
|||
msgid "dashboard.webhooks.create.success"
|
||||
msgstr "Webhook creado con éxito"
|
||||
|
||||
#: src/app/main/errors.cljs
|
||||
msgid "errors.feature-not-supported"
|
||||
msgstr "Caracteristica no soportada: '%s'."
|
||||
|
||||
#: src/app/main/errors.cljs
|
||||
msgid "errors.max-quote-reached"
|
||||
msgstr "Ha alcalzando el maximo de la quota '%s'. Contacte con soporte tecnico."
|
||||
|
||||
#: src/app/main/errors.cljs
|
||||
msgid "errors.feature-mismatch"
|
||||
msgstr "Parece que esta abriendo un fichero con la caracteristica '%s' habilitada pero la aplicacion web de penpot que esta usando no tiene soporte para ella o esta deshabilitada."
|
||||
|
||||
msgid "errors.webhooks.timeout"
|
||||
msgstr "Timeout"
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue