penpot/backend/test/backend_tests/srepl_main_test.clj
Andrey Antukh 3bbc1c3079 ♻️ Make cascade helpers stream rows and enqueue tasks
The four `process-deleted-*-cascade` helpers of `app.srepl.main`
loaded every soft-deleted row of a table into memory (`db/exec!`)
and ran `:delete-object` inline (`wrk/invoke!`), so they could not
be used against production: `file` alone has 2.1M deleted rows.

They now share a single `submit-deleted-cascade!` that reads the
rows with a server-side cursor (`db/plan`, in a read-only
transaction) and submits one task per row, so the work goes
through the workers like any other task. The row id goes as
`label` with `dedupe` on, and the helpers accept `:limit`, `:ids`
and `:dry-run?` to run bounded batches. Public names and their
zero-arity form are unchanged.

Tests cover the task params, the row's own date (not `now`), the
`limit`/`ids`/`dry-run?` options and that each helper reads its own
table.

AI-assisted-by: deepseek-v4.1-flash
2026-10-13 14:33:32 +02:00

153 lines
6.1 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns backend-tests.srepl-main-test
(:require
[app.common.time :as ct]
[app.srepl.main :as srepl]
[app.system :as sys]
[app.worker :as wrk]
[backend-tests.helpers :as th]
[clojure.test :as t]))
(t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset)
;; --- HELPERS
(defn- capture-submissions!
"Runs `f` with `wrk/submit!` replaced by a collector, and returns the option
maps it was called with, in order."
[f]
(let [submitted (atom [])]
(with-redefs [wrk/submit! (fn [opts]
(swap! submitted conj opts)
(get-in opts [::wrk/params :id]))]
(f))
@submitted))
(defn- params-of
[submitted]
(mapv ::wrk/params submitted))
(defn- create-deleted-profile!
[i deleted-at]
(let [profile (th/create-profile* i)]
(th/db-update! :profile {:deleted-at deleted-at} {:id (:id profile)})
profile))
;; --- TESTS
(t/deftest parse-emails
(t/is (= ["some@example.com"]
(srepl/parse-emails "some@example.com")))
(t/is (= ["some@example.com" "other@example.com"]
(srepl/parse-emails "some@example.com,other@example.com")))
(t/is (= ["some@example.com" "other@example.com"]
(srepl/parse-emails " some@example.com , other@example.com ,")))
(t/is (= ["some@example.com" "other@example.com"]
(srepl/parse-emails ["some@example.com" "other@example.com"])))
(t/is (= [] (srepl/parse-emails ",")))
(t/is (thrown? clojure.lang.ExceptionInfo
(srepl/parse-emails 42))))
(t/deftest cascade-enqueues-one-delete-object-task-per-deleted-row
(let [deleted (create-deleted-profile! 991 (ct/in-past {:days 30}))
_ (th/create-profile* 992)
submitted (capture-submissions!
#(#'srepl/submit-deleted-cascade
th/*system* {:table "profile" :object :profile}))]
(t/testing "only the soft-deleted row is processed"
(t/is (= 1 (count submitted))))
(t/testing "the enqueued task is a delete-object for that row"
(let [opts (first submitted)]
(t/is (= :delete-object (::wrk/task opts)))
(t/is (= {:object :profile :id (:id deleted)}
(select-keys (::wrk/params opts) [:object :id])))))
(t/testing "the date is the row's own, not now"
(t/is (ct/is-before? (:deleted-at (::wrk/params (first submitted)))
(ct/in-past {:days 29}))))
(t/testing "label is the row id and dedupe is on"
(t/is (= (str (:id deleted)) (::wrk/label (first submitted))))
(t/is (true? (::wrk/dedupe (first submitted)))))))
(t/deftest cascade-honours-limit-and-ids
(let [p1 (create-deleted-profile! 993 (ct/in-past {:days 10}))
_ (create-deleted-profile! 994 (ct/in-past {:days 9}))
_ (create-deleted-profile! 995 (ct/in-past {:days 8}))]
(t/testing "limit bounds how many rows are read"
(let [submitted (capture-submissions!
#(#'srepl/submit-deleted-cascade
th/*system* {:table "profile" :object :profile :limit 2}))]
(t/is (= 2 (count submitted)))))
(t/testing "ids restricts the run to the given rows"
(let [submitted (capture-submissions!
#(#'srepl/submit-deleted-cascade
th/*system* {:table "profile" :object :profile :ids [(:id p1)]}))]
(t/is (= [{:object :profile :id (:id p1)}]
(mapv #(select-keys % [:object :id]) (params-of submitted))))))))
(t/deftest cascade-dry-run-counts-without-enqueueing
(let [_ (create-deleted-profile! 996 (ct/in-past {:days 10}))
submitted (atom [])
result (with-redefs [wrk/submit! (fn [opts] (swap! submitted conj opts))]
(#'srepl/submit-deleted-cascade
th/*system* {:table "profile" :object :profile :dry-run? true}))]
(t/is (= 1 (:found result)))
(t/is (= 0 (:submitted result)))
(t/is (empty? @submitted))))
(t/deftest public-helpers-read-their-own-table
(let [profile (th/create-profile* 997)
team (th/create-team* 997 {:profile-id (:id profile)})
project (th/create-project* 997 {:profile-id (:id profile)
:team-id (:id team)})
file (th/create-file* 997 {:profile-id (:id profile)
:project-id (:id project)})
old (ct/in-past {:days 10})]
(th/db-update! :profile {:deleted-at old} {:id (:id profile)})
(th/db-update! :team {:deleted-at old} {:id (:id team)})
(th/db-update! :project {:deleted-at old} {:id (:id project)})
(th/db-update! :file {:deleted-at old} {:id (:id file)})
(with-redefs [sys/system th/*system*]
(let [ids (fn [submitted] (into #{} (map :id (params-of submitted))))]
(t/testing "profiles helper"
(let [submitted (capture-submissions! #(srepl/process-deleted-profiles-cascade))]
(t/is (contains? (ids submitted) (:id profile)))))
(t/testing "teams helper"
(let [submitted (capture-submissions! #(srepl/process-deleted-teams-cascade))]
(t/is (contains? (ids submitted) (:id team)))))
(t/testing "projects helper"
(let [submitted (capture-submissions! #(srepl/process-deleted-projects-cascade))]
(t/is (contains? (ids submitted) (:id project)))))
(t/testing "files helper"
(let [submitted (capture-submissions! #(srepl/process-deleted-files-cascade))]
(t/is (contains? (ids submitted) (:id file)))))
(t/testing "the helpers accept options"
(let [submitted (capture-submissions!
#(srepl/process-deleted-files-cascade :ids [(:id file)]))]
(t/is (= [{:object :file :id (:id file)}]
(mapv #(select-keys % [:object :id]) (params-of submitted))))))))))