♻️ Reimplement GC mechanism for penpot database objects.

This commit is contained in:
Andrey Antukh 2021-06-07 16:51:09 +02:00 committed by Andrés Moya
parent 71c4145ea2
commit 860e0227af
20 changed files with 437 additions and 104 deletions

View file

@ -172,18 +172,21 @@
{:cron #app/cron "0 0 * * * ?" ;; hourly {:cron #app/cron "0 0 * * * ?" ;; hourly
:task :file-xlog-gc} :task :file-xlog-gc}
{:cron #app/cron "0 0 1 * * ?" ;; daily (1 hour shift) {:cron #app/cron "0 0 0 * * ?" ;; daily
:task :storage-deleted-gc} :task :storage-deleted-gc}
{:cron #app/cron "0 0 2 * * ?" ;; daily (2 hour shift) {:cron #app/cron "0 0 0 * * ?" ;; daily
:task :storage-touched-gc} :task :storage-touched-gc}
{:cron #app/cron "0 0 3 * * ?" ;; daily (3 hour shift) {:cron #app/cron "0 0 0 * * ?" ;; daily
:task :session-gc} :task :session-gc}
{:cron #app/cron "0 0 * * * ?" ;; hourly {:cron #app/cron "0 0 * * * ?" ;; hourly
:task :storage-recheck} :task :storage-recheck}
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :objects-gc}
{:cron #app/cron "0 0 0 * * ?" ;; daily {:cron #app/cron "0 0 0 * * ?" ;; daily
:task :tasks-gc} :task :tasks-gc}
@ -203,6 +206,7 @@
{:metrics (ig/ref :app.metrics/metrics) {:metrics (ig/ref :app.metrics/metrics)
:tasks :tasks
{:sendmail (ig/ref :app.emails/sendmail-handler) {:sendmail (ig/ref :app.emails/sendmail-handler)
:objects-gc (ig/ref :app.tasks.objects-gc/handler)
:delete-object (ig/ref :app.tasks.delete-object/handler) :delete-object (ig/ref :app.tasks.delete-object/handler)
:delete-profile (ig/ref :app.tasks.delete-profile/handler) :delete-profile (ig/ref :app.tasks.delete-profile/handler)
:file-media-gc (ig/ref :app.tasks.file-media-gc/handler) :file-media-gc (ig/ref :app.tasks.file-media-gc/handler)
@ -236,6 +240,11 @@
{:pool (ig/ref :app.db/pool) {:pool (ig/ref :app.db/pool)
:storage (ig/ref :app.storage/storage)} :storage (ig/ref :app.storage/storage)}
:app.tasks.objects-gc/handler
{:pool (ig/ref :app.db/pool)
:storage (ig/ref :app.storage/storage)
:max-age cf/deletion-delay}
:app.tasks.delete-profile/handler :app.tasks.delete-profile/handler
{:pool (ig/ref :app.db/pool)} {:pool (ig/ref :app.db/pool)}

View file

@ -175,6 +175,15 @@
{:name "0055-mod-file-media-object-table" {:name "0055-mod-file-media-object-table"
:fn (mg/resource "app/migrations/sql/0055-mod-file-media-object-table.sql")} :fn (mg/resource "app/migrations/sql/0055-mod-file-media-object-table.sql")}
{:name "0056-add-missing-index-on-deleted-at"
:fn (mg/resource "app/migrations/sql/0056-add-missing-index-on-deleted-at.sql")}
{:name "0057-del-profile-on-delete-trigger"
:fn (mg/resource "app/migrations/sql/0057-del-profile-on-delete-trigger.sql")}
{:name "0058-del-team-on-delete-trigger"
:fn (mg/resource "app/migrations/sql/0058-del-team-on-delete-trigger.sql")}
]) ])

View file

@ -0,0 +1,15 @@
CREATE INDEX profile_deleted_at_idx
ON profile(deleted_at, id)
WHERE deleted_at IS NOT NULL;
CREATE INDEX project_deleted_at_idx
ON project(deleted_at, id)
WHERE deleted_at IS NOT NULL;
CREATE INDEX team_deleted_at_idx
ON team(deleted_at, id)
WHERE deleted_at IS NOT NULL;
CREATE INDEX team_font_variant_deleted_at_idx
ON team_font_variant(deleted_at, id)
WHERE deleted_at IS NOT NULL;

View file

@ -0,0 +1,2 @@
DROP TRIGGER profile__on_delete__tgr ON profile CASCADE;
DROP FUNCTION on_delete_profile ();

View file

@ -0,0 +1,2 @@
DROP TRIGGER team__on_delete__tgr ON team CASCADE;
DROP FUNCTION on_delete_team ();

View file

@ -15,7 +15,7 @@
[app.rpc.mutations.profile :as profile] [app.rpc.mutations.profile :as profile]
[app.setup.initial-data :as sid] [app.setup.initial-data :as sid]
[app.util.services :as sv] [app.util.services :as sv]
[app.worker :as wrk] [app.util.time :as dt]
[buddy.core.codecs :as bc] [buddy.core.codecs :as bc]
[buddy.core.nonce :as bn] [buddy.core.nonce :as bn]
[clojure.spec.alpha :as s])) [clojure.spec.alpha :as s]))
@ -35,6 +35,7 @@
:email email :email email
:fullname fullname :fullname fullname
:is-demo true :is-demo true
:deleted-at (dt/in-future cfg/deletion-delay)
:password password :password password
:props {:onboarding-viewed true}}] :props {:onboarding-viewed true}}]
@ -48,12 +49,6 @@
(#'profile/create-profile-relations conn) (#'profile/create-profile-relations conn)
(sid/load-initial-project! conn)) (sid/load-initial-project! conn))
;; Schedule deletion of the demo profile
(wrk/submit! {::wrk/task :delete-profile
::wrk/delay cfg/deletion-delay
::wrk/conn conn
:profile-id id})
(with-meta {:email email (with-meta {:email email
:password password} :password password}
{::audit/profile-id id})))) {::audit/profile-id id}))))

View file

@ -11,7 +11,6 @@
[app.common.pages.migrations :as pmg] [app.common.pages.migrations :as pmg]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cfg]
[app.db :as db] [app.db :as db]
[app.rpc.permissions :as perms] [app.rpc.permissions :as perms]
[app.rpc.queries.files :as files] [app.rpc.queries.files :as files]
@ -19,7 +18,6 @@
[app.util.blob :as blob] [app.util.blob :as blob]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s])) [clojure.spec.alpha :as s]))
;; --- Helpers & Specs ;; --- Helpers & Specs
@ -121,14 +119,6 @@
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id id) (files/check-edition-permissions! conn profile-id id)
;; Schedule object deletion
(wrk/submit! {::wrk/task :delete-object
::wrk/delay cfg/deletion-delay
::wrk/conn conn
:id id
:type :file})
(mark-file-deleted conn params))) (mark-file-deleted conn params)))
(defn mark-file-deleted (defn mark-file-deleted

View file

@ -104,21 +104,10 @@
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(teams/check-edition-permissions! conn profile-id team-id) (teams/check-edition-permissions! conn profile-id team-id)
(let [items (db/query conn :team-font-variant
{:font-id id :team-id team-id}
{:for-update true})]
(doseq [item items]
;; Schedule object deletion
(wrk/submit! {::wrk/task :delete-object
::wrk/delay cf/deletion-delay
::wrk/conn conn
:id (:id item)
:type :team-font-variant}))
(db/update! conn :team-font-variant (db/update! conn :team-font-variant
{:deleted-at (dt/now)} {:deleted-at (dt/now)}
{:font-id id :team-id team-id}) {:font-id id :team-id team-id})
nil))) nil))
;; --- DELETE FONT VARIANT ;; --- DELETE FONT VARIANT

View file

@ -22,7 +22,6 @@
[app.storage :as sto] [app.storage :as sto]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as wrk]
[buddy.hashers :as hashers] [buddy.hashers :as hashers]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str])) [cuerdas.core :as str]))
@ -179,9 +178,9 @@
:valid false}))) :valid false})))
(defn create-profile (defn create-profile
"Create the profile entry on the database with limited input "Create the profile entry on the database with limited input filling
filling all the other fields with defaults." all the other fields with defaults."
[conn {:keys [id fullname email password is-active is-muted is-demo opts] [conn {:keys [id fullname email password is-active is-muted is-demo opts deleted-at]
:or {is-active false is-muted false is-demo false} :or {is-active false is-muted false is-demo false}
:as params}] :as params}]
(let [id (or id (uuid/next)) (let [id (or id (uuid/next))
@ -193,6 +192,7 @@
:email (str/lower email) :email (str/lower email)
:auth-backend "penpot" :auth-backend "penpot"
:password password :password password
:deleted-at deleted-at
:props props :props props
:is-active is-active :is-active is-active
:is-muted is-muted :is-muted is-muted
@ -264,7 +264,8 @@
(let [profile (->> (profile/retrieve-profile-data-by-email conn email) (let [profile (->> (profile/retrieve-profile-data-by-email conn email)
(validate-profile) (validate-profile)
(profile/strip-private-attrs) (profile/strip-private-attrs)
(profile/populate-additional-data conn))] (profile/populate-additional-data conn))
profile (update profile :props db/decode-transit-pgobject)]
(if-let [token (:invitation-token params)] (if-let [token (:invitation-token params)]
;; If the request comes with an invitation token, this means ;; If the request comes with an invitation token, this means
;; that user wants to accept it with different user. A very ;; that user wants to accept it with different user. A very
@ -619,12 +620,6 @@
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(check-can-delete-profile! conn profile-id) (check-can-delete-profile! conn profile-id)
;; Schedule a complete deletion of profile
(wrk/submit! {::wrk/task :delete-profile
::wrk/delay cfg/deletion-delay
::wrk/conn conn
:profile-id profile-id})
(db/update! conn :profile (db/update! conn :profile
{:deleted-at (dt/now)} {:deleted-at (dt/now)}
{:id profile-id}) {:id profile-id})

View file

@ -8,14 +8,12 @@
(:require (:require
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cfg]
[app.db :as db] [app.db :as db]
[app.rpc.permissions :as perms] [app.rpc.permissions :as perms]
[app.rpc.queries.projects :as proj] [app.rpc.queries.projects :as proj]
[app.rpc.queries.teams :as teams] [app.rpc.queries.teams :as teams]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s])) [clojure.spec.alpha :as s]))
;; --- Helpers & Specs ;; --- Helpers & Specs
@ -123,14 +121,6 @@
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(proj/check-edition-permissions! conn profile-id id) (proj/check-edition-permissions! conn profile-id id)
;; Schedule object deletion
(wrk/submit! {::wrk/task :delete-object
::wrk/delay cfg/deletion-delay
::wrk/conn conn
:id id
:type :project})
(db/update! conn :project (db/update! conn :project
{:deleted-at (dt/now)} {:deleted-at (dt/now)}
{:id id}) {:id id})

View file

@ -10,7 +10,6 @@
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cfg]
[app.db :as db] [app.db :as db]
[app.emails :as eml] [app.emails :as eml]
[app.media :as media] [app.media :as media]
@ -21,7 +20,6 @@
[app.storage :as sto] [app.storage :as sto]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[datoteka.core :as fs])) [datoteka.core :as fs]))
@ -135,13 +133,6 @@
(ex/raise :type :validation (ex/raise :type :validation
:code :only-owner-can-delete-team)) :code :only-owner-can-delete-team))
;; Schedule object deletion
(wrk/submit! {::wrk/task :delete-object
::wrk/delay cfg/deletion-delay
::wrk/conn conn
:id id
:type :team})
(db/update! conn :team (db/update! conn :team
{:deleted-at (dt/now)} {:deleted-at (dt/now)}
{:id id}) {:id id})

View file

@ -11,7 +11,8 @@
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.db :as db] [app.db :as db]
[app.util.services :as sv] [app.util.services :as sv]
[clojure.spec.alpha :as s])) [clojure.spec.alpha :as s]
[cuerdas.core :as str]))
;; --- Helpers & Specs ;; --- Helpers & Specs
@ -90,16 +91,11 @@
profile)) profile))
(def sql:retrieve-profile-by-email
"select p.* from profile as p
where p.email = lower(?)
and p.deleted_at is null")
(defn retrieve-profile-data-by-email (defn retrieve-profile-data-by-email
[conn email] [conn email]
(let [sql [sql:retrieve-profile-by-email email]] (try
(some-> (db/exec-one! conn sql) (db/get-by-params conn :profile {:email (str/lower email)})
(decode-profile-row)))) (catch Exception _e)))
;; --- Attrs Helpers ;; --- Attrs Helpers

View file

@ -4,6 +4,9 @@
;; ;;
;; Copyright (c) UXBOX Labs SL ;; Copyright (c) UXBOX Labs SL
;; TODO: DEPRECATED
;; Should be removed in the 1.8.x
(ns app.tasks.delete-object (ns app.tasks.delete-object
"Generic task for permanent deletion of objects." "Generic task for permanent deletion of objects."
(:require (:require

View file

@ -14,6 +14,9 @@
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[integrant.core :as ig])) [integrant.core :as ig]))
;; TODO: DEPRECATED
;; Should be removed in the 1.8.x
(declare delete-profile-data) (declare delete-profile-data)
;; --- INIT ;; --- INIT

View file

@ -100,6 +100,7 @@
:id (:id mobj) :id (:id mobj)
:media-id (:media-id mobj) :media-id (:media-id mobj)
:thumbnail-id (:thumbnail-id mobj)) :thumbnail-id (:thumbnail-id mobj))
;; NOTE: deleting the file-media-object in the database ;; NOTE: deleting the file-media-object in the database
;; automatically marks as toched the referenced storage ;; automatically marks as toched the referenced storage
;; objects. The touch mechanism is needed because many files can ;; objects. The touch mechanism is needed because many files can

View file

@ -0,0 +1,152 @@
;; 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) UXBOX Labs SL
(ns app.tasks.objects-gc
"A maintenance task that performs a general purpose garbage collection
of deleted objects."
(:require
[app.db :as db]
[app.storage :as sto]
[app.util.logging :as l]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig]))
(def target-tables
["profile"
"team"
"file"
"project"
"team_font_variant"])
(defmulti delete-objects :table)
(def sql:delete-objects
"with deleted as (
select id from %(table)s
where deleted_at is not null
and deleted_at < now() - ?::interval
order by deleted_at
limit %(limit)s
)
delete from %(table)s
where id in (select id from deleted)
returning *")
;; --- IMPL: generic object deletion
(defmethod delete-objects :default
[{:keys [conn max-age table] :as cfg}]
(let [sql (str/fmt sql:delete-objects
{:table table :limit 50})
result (db/exec! conn [sql max-age])]
(doseq [{:keys [id] :as item} result]
(l/trace :action "delete object" :table table :id id))
(count result)))
;; --- IMPL: team-font-variant deletion
(defmethod delete-objects "team_font_variant"
[{:keys [conn max-age storage table] :as cfg}]
(let [sql (str/fmt sql:delete-objects
{:table table :limit 50})
fonts (db/exec! conn [sql max-age])
storage (assoc storage :conn conn)]
(doseq [{:keys [id] :as font} fonts]
(l/trace :action "delete object" :table table :id id)
(some->> (:woff1-file-id font) (sto/del-object storage))
(some->> (:woff2-file-id font) (sto/del-object storage))
(some->> (:otf-file-id font) (sto/del-object storage))
(some->> (:ttf-file-id font) (sto/del-object storage)))
(count fonts)))
;; --- IMPL: team deletion
(defmethod delete-objects "team"
[{:keys [conn max-age storage table] :as cfg}]
(let [sql (str/fmt sql:delete-objects
{:table table :limit 50})
teams (db/exec! conn [sql max-age])
storage (assoc storage :conn conn)]
(doseq [{:keys [id] :as team} teams]
(l/trace :action "delete object" :table table :id id)
(some->> (:photo-id team) (sto/del-object storage)))
(count teams)))
;; --- IMPL: profile deletion
(def sql:retrieve-deleted-profiles
"select id, photo_id from profile
where deleted_at is not null
and deleted_at < now() - ?::interval
order by deleted_at
limit %(limit)s
for update")
(def sql:mark-owned-teams-deleted
"with owned as (
select tpr.team_id as id
from team_profile_rel as tpr
where tpr.is_owner is true
and tpr.profile_id = ?
)
update team set deleted_at = now() - ?::interval
where id in (select id from owned)")
(defmethod delete-objects "profile"
[{:keys [conn max-age storage table] :as cfg}]
(let [sql (str/fmt sql:retrieve-deleted-profiles {:limit 50})
profiles (db/exec! conn [sql max-age])
storage (assoc storage :conn conn)]
(doseq [{:keys [id] :as profile} profiles]
(l/trace :action "delete object" :table table :id id)
;; Mark the owned teams as deleted; this enables them to be procesed
;; in the same transaction in the "team" table step.
(db/exec-one! conn [sql:mark-owned-teams-deleted id max-age])
;; Mark as deleted the storage object related with the photo-id
;; field.
(some->> (:photo-id profile) (sto/del-object storage))
;; And finally, permanently delete the profile.
(db/delete! conn :profile {:id id}))
(count profiles)))
;; --- INIT
(defn- process-table
[{:keys [table] :as cfg}]
(loop [n 0]
(let [res (delete-objects cfg)]
(if (pos? res)
(recur (+ n res))
(l/debug :hint "table gc summary" :table table :deleted n)))))
(s/def ::max-age ::dt/duration)
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req-un [::db/pool ::sto/storage ::max-age]))
(defmethod ig/init-key ::handler
[_ {:keys [pool max-age] :as cfg}]
(fn [task]
;; Checking first on task argument allows properly testing it.
(let [max-age (get task :max-age max-age)]
(db/with-atomic [conn pool]
(let [max-age (db/interval max-age)
cfg (-> cfg
(assoc :max-age max-age)
(assoc :conn conn))]
(doseq [table target-tables]
(process-table (assoc cfg :table table))))))))

View file

@ -11,6 +11,7 @@
[app.http :as http] [app.http :as http]
[app.storage :as sto] [app.storage :as sto]
[app.test-helpers :as th] [app.test-helpers :as th]
[app.util.time :as dt]
[clojure.test :as t] [clojure.test :as t]
[datoteka.core :as fs])) [datoteka.core :as fs]))
@ -337,3 +338,69 @@
(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 deletion-test
(let [task (:app.tasks.objects-gc/handler th/*system*)
profile1 (th/create-profile* 1)
file (th/create-file* 1 {:project-id (:default-project-id profile1)
:profile-id (:id profile1)})]
;; file is not deleted because it does not meet all
;; conditions to be deleted.
(let [result (task {:max-age (dt/duration 0)})]
(t/is (nil? result)))
;; query the list of files
(let [data {::th/type :project-files
:project-id (:default-project-id profile1)
:profile-id (:id profile1)}
out (th/query! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= 1 (count result)))))
;; Request file to be deleted
(let [params {::th/type :delete-file
:id (:id file)
:profile-id (:id profile1)}
out (th/mutation! params)]
(t/is (nil? (:error out))))
;; query the list of files after soft deletion
(let [data {::th/type :project-files
:project-id (:default-project-id profile1)
:profile-id (:id profile1)}
out (th/query! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= 0 (count result)))))
;; run permanent deletion (should be noop)
(let [result (task {:max-age (dt/duration {:minutes 1})})]
(t/is (nil? result)))
;; query the list of file libraries of a after hard deletion
(let [data {::th/type :file-libraries
:file-id (:id file)
:profile-id (:id profile1)}
out (th/query! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= 0 (count result)))))
;; run permanent deletion
(let [result (task {:max-age (dt/duration 0)})]
(t/is (nil? result)))
;; query the list of file libraries of a after hard deletion
(let [data {::th/type :file-libraries
:file-id (:id file)
:profile-id (:id profile1)}
out (th/query! data)]
;; (th/print-result! out)
(let [error (:error out)
error-data (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type error-data) :not-found))))
))

View file

@ -9,6 +9,7 @@
[app.db :as db] [app.db :as db]
[app.rpc.mutations.profile :as profile] [app.rpc.mutations.profile :as profile]
[app.test-helpers :as th] [app.test-helpers :as th]
[app.util.time :as dt]
[clojure.java.io :as io] [clojure.java.io :as io]
[clojure.test :as t] [clojure.test :as t]
[cuerdas.core :as str] [cuerdas.core :as str]
@ -117,7 +118,7 @@
)) ))
(t/deftest profile-deletion-simple (t/deftest profile-deletion-simple
(let [task (:app.tasks.delete-profile/handler th/*system*) (let [task (:app.tasks.objects-gc/handler th/*system*)
prof (th/create-profile* 1) prof (th/create-profile* 1)
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)
@ -125,23 +126,14 @@
;; profile is not deleted because it does not meet all ;; profile is not deleted because it does not meet all
;; conditions to be deleted. ;; conditions to be deleted.
(let [result (task {:props {:profile-id (:id prof)}})] (let [result (task {:max-age (dt/duration 0)})]
(t/is (nil? result))) (t/is (nil? result)))
;; Request profile to be deleted ;; Request profile to be deleted
(with-mocks [mock {:target 'app.worker/submit! :return nil}]
(let [params {::th/type :delete-profile (let [params {::th/type :delete-profile
:profile-id (:id prof)} :profile-id (:id prof)}
out (th/mutation! params)] out (th/mutation! params)]
(t/is (nil? (:error out))) (t/is (nil? (:error out))))
;; check the mock
(let [mock (deref mock)
mock-params (first (:call-args mock))]
(t/is (:called? mock))
(t/is (= 1 (:call-count mock)))
(t/is (= :delete-profile (:app.worker/task mock-params)))
(t/is (= (:id prof) (:profile-id mock-params))))))
;; query files after profile soft deletion ;; query files after profile soft deletion
(let [params {::th/type :files (let [params {::th/type :files
@ -153,8 +145,8 @@
(t/is (= 1 (count (:result out))))) (t/is (= 1 (count (:result out)))))
;; execute permanent deletion task ;; execute permanent deletion task
(let [result (task {:props {:profile-id (:id prof)}})] (let [result (task {:max-age (dt/duration "-1m")})]
(t/is (true? result))) (t/is (nil? result)))
;; query profile after delete ;; query profile after delete
(let [params {::th/type :profile (let [params {::th/type :profile
@ -165,17 +157,6 @@
error-data (ex-data error)] error-data (ex-data error)]
(t/is (th/ex-info? error)) (t/is (th/ex-info? error))
(t/is (= (:type error-data) :not-found)))) (t/is (= (:type error-data) :not-found))))
;; query files after profile soft deletion
(let [params {::th/type :files
:project-id (:default-project-id prof)
:profile-id (:id prof)}
out (th/query! params)]
;; (th/print-result! out)
(let [error (:error out)
error-data (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type error-data) :not-found))))
)) ))
(t/deftest registration-domain-whitelist (t/deftest registration-domain-whitelist

View file

@ -10,8 +10,8 @@
[app.db :as db] [app.db :as db]
[app.http :as http] [app.http :as http]
[app.test-helpers :as th] [app.test-helpers :as th]
[clojure.test :as t] [app.util.time :as dt]
[promesa.core :as p])) [clojure.test :as t]))
(t/use-fixtures :once th/state-init) (t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset) (t/use-fixtures :each th/database-reset)
@ -170,3 +170,71 @@
(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 test-deletion
(let [task (:app.tasks.objects-gc/handler th/*system*)
profile1 (th/create-profile* 1)
project (th/create-project* 1 {:team-id (:default-team-id profile1)
:profile-id (:id profile1)})]
;; project is not deleted because it does not meet all
;; conditions to be deleted.
(let [result (task {:max-age (dt/duration 0)})]
(t/is (nil? result)))
;; query the list of projects
(let [data {::th/type :projects
:team-id (:default-team-id profile1)
:profile-id (:id profile1)}
out (th/query! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= 2 (count result)))))
;; Request project to be deleted
(let [params {::th/type :delete-project
:id (:id project)
:profile-id (:id profile1)}
out (th/mutation! params)]
(t/is (nil? (:error out))))
;; query the list of projects after soft deletion
(let [data {::th/type :projects
:team-id (:default-team-id profile1)
:profile-id (:id profile1)}
out (th/query! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= 1 (count result)))))
;; run permanent deletion (should be noop)
(let [result (task {:max-age (dt/duration {:minutes 1})})]
(t/is (nil? result)))
;; query the list of files of a after soft deletion
(let [data {::th/type :project-files
:project-id (:id project)
:profile-id (:id profile1)}
out (th/query! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= 0 (count result)))))
;; run permanent deletion
(let [result (task {:max-age (dt/duration 0)})]
(t/is (nil? result)))
;; query the list of files of a after hard deletion
(let [data {::th/type :project-files
:project-id (:id project)
:profile-id (:id profile1)}
out (th/query! data)]
;; (th/print-result! out)
(let [error (:error out)
error-data (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type error-data) :not-found))))
))

View file

@ -11,6 +11,7 @@
[app.http :as http] [app.http :as http]
[app.storage :as sto] [app.storage :as sto]
[app.test-helpers :as th] [app.test-helpers :as th]
[app.util.time :as dt]
[clojure.test :as t] [clojure.test :as t]
[datoteka.core :as fs] [datoteka.core :as fs]
[mockery.core :refer [with-mocks]])) [mockery.core :refer [with-mocks]]))
@ -80,6 +81,80 @@
))) )))
(t/deftest test-deletion
(let [task (:app.tasks.objects-gc/handler th/*system*)
profile1 (th/create-profile* 1 {:is-active true})
team (th/create-team* 1 {:profile-id (:id profile1)})
pool (:app.db/pool th/*system*)
data {::th/type :delete-team
:team-id (:id team)
:profile-id (:id profile1)}]
;; team is not deleted because it does not meet all
;; conditions to be deleted.
(let [result (task {:max-age (dt/duration 0)})]
(t/is (nil? result)))
;; query the list of teams
(let [data {::th/type :teams
:profile-id (:id profile1)}
out (th/query! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= 2 (count result)))
(t/is (= (:id team) (get-in result [1 :id])))
(t/is (= (:default-team-id profile1) (get-in result [0 :id])))))
;; Request team to be deleted
(let [params {::th/type :delete-team
:id (:id team)
:profile-id (:id profile1)}
out (th/mutation! params)]
(t/is (nil? (:error out))))
;; query the list of teams after soft deletion
(let [data {::th/type :teams
:profile-id (:id profile1)}
out (th/query! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= 1 (count result)))
(t/is (= (:default-team-id profile1) (get-in result [0 :id])))))
;; run permanent deletion (should be noop)
(let [result (task {:max-age (dt/duration {:minutes 1})})]
(t/is (nil? result)))
;; query the list of projects of a after hard deletion
(let [data {::th/type :projects
:team-id (:id team)
:profile-id (:id profile1)}
out (th/query! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= 0 (count result)))))
;; run permanent deletion
(let [result (task {:max-age (dt/duration 0)})]
(t/is (nil? result)))
;; query the list of projects of a after hard deletion
(let [data {::th/type :projects
:team-id (:id team)
:profile-id (:id profile1)}
out (th/query! data)]
;; (th/print-result! out)
(let [error (:error out)
error-data (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type error-data) :not-found))))
))