penpot/backend/test/backend_tests/storage_config_test.clj
Andrey Antukh 6eece49f69 🐛 Keep objects whose storage target is no longer configured
An object whose :storage-target id is not in the S3 routes configuration
can no longer be located. Reads and serving now fail with
:invalid-storage-target instead of silently using the default bucket,
and garbage collection refuses to delete the row: it logs an error and
parks the row (deleted_at +1 day, no deletion attempts, no give-up)
until the target is configured again. Legacy rows (no target) and the
default target keep working and deleting normally.

Also addresses review findings: per-target dedup isolation and
stale-repair coverage, legacy-row GC coverage through the real task
paths, a single declared-target schema, closing already-built S3
clients when target init fails, and loader edge pins. Docs and memory
updated.

Closes #11630

AI-assisted-by: deepseek-flash
2026-09-10 21:38:38 +00:00

98 lines
3.2 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns backend-tests.storage-config-test
(:require
[app.common.uri :as u]
[app.config :as cf]
[app.storage.config :as sto.config]
[clojure.java.io :as io]
[clojure.test :as t]))
(defn- write-routes!
[content]
(let [file (java.io.File/createTempFile "storage-routes" ".edn")]
(spit file content)
file))
(defn- delete!
[file]
(io/delete-file file true))
(defn- load-with
[config]
(binding [cf/config (merge cf/config
{:objects-storage-backend :s3}
config)]
(sto.config/load)))
(defn- load-error
"Runs `load` against a routes file with `content` and returns the raised
throwable, or nil when it succeeds."
[content config]
(let [file (write-routes! content)]
(try
(try
(load-with (assoc config :objects-storage-s3-routes-file (.getAbsolutePath file)))
nil
(catch Throwable cause cause))
(finally
(delete! file)))))
(t/deftest returns-empty-when-unset
(t/is (= {:targets nil :routes nil}
(load-with {:objects-storage-s3-routes-file nil}))))
(t/deftest loads-and-normalizes-routes
(let [file (write-routes!
"{:targets {:temp {:bucket \"penpot-temp\" :endpoint \"https://s3.example.com\"}} :routes {\"tempfile\" :temp}}")]
(try
(let [result (load-with {:objects-storage-s3-routes-file (.getAbsolutePath file)})]
(t/is (= #{:temp} (set (keys (:targets result)))))
(t/is (= "penpot-temp" (get-in result [:targets :temp :bucket])))
(t/is (u/uri? (get-in result [:targets :temp :endpoint])))
(t/is (= {"tempfile" :temp} (:routes result))))
(finally
(delete! file)))))
(t/deftest rejects-reserved-default-target
(t/is (some? (load-error "{:targets {:default {:bucket \"x\"}}}"
{}))))
(t/deftest rejects-unknown-route-target
(t/is (some? (load-error
"{:targets {:temp {:bucket \"x\"}} :routes {\"tempfile\" :missing}}"
{}))))
(t/deftest rejects-invalid-semantic-bucket
(t/is (some? (load-error
"{:targets {:temp {:bucket \"x\"}} :routes {\"not-a-bucket\" :temp}}"
{}))))
(t/deftest rejects-unreadable-file
(t/is (some? (load-error "{:targets" {}))))
(t/deftest rejects-routing-when-backend-is-not-s3
(t/is (some? (load-error
"{:targets {:temp {:bucket \"x\"}} :routes {\"tempfile\" :temp}}"
{:objects-storage-backend :fs}))))
(t/deftest rejects-route-pointing-to-default
(let [err (load-error
"{:targets {:temp {:bucket \"x\"}} :routes {\"tempfile\" :default}}"
{})]
(t/is (some? err))
(t/is (= :unknown-storage-target (:code (ex-data err))))))
(t/deftest loads-empty-targets-map
(let [file (write-routes! "{:targets {}}")]
(try
(let [result (load-with {:objects-storage-s3-routes-file (.getAbsolutePath file)})]
(t/is (= {} (:targets result)))
(t/is (nil? (:routes result))))
(finally
(delete! file)))))