Merge master into enhance/i18n

This commit is contained in:
Mega Yu
2026-04-14 16:42:19 +08:00
257 changed files with 29858 additions and 7658 deletions

View File

@@ -0,0 +1,35 @@
(ns frontend.components.property.property-test
(:require [cljs.test :refer [deftest is]]
[frontend.components.property :as property-component]))
(deftest sanitize-property-values-for-display-filters-recycled-entity-values-test
(let [active-value {:db/id 101
:block/title "Active"}
recycled-value {:db/id 102
:block/title "Recycled"
:logseq.property/deleted-at 1}
{:keys [properties recycled-only-property-ids]}
(#'property-component/sanitize-property-values-for-display
{:user.property/node #{active-value recycled-value}
:user.property/single recycled-value
:user.property/scalar "ok"})]
(is (= #{active-value}
(:user.property/node properties)))
(is (nil? (:user.property/single properties)))
(is (= "ok" (:user.property/scalar properties)))
(is (= #{:user.property/single}
recycled-only-property-ids))))
(deftest sanitize-property-values-for-display-marks-all-recycled-coll-as-hidden-test
(let [recycled-a {:db/id 201
:block/title "Recycled A"
:logseq.property/deleted-at 1}
recycled-b {:db/id 202
:block/title "Recycled B"
:logseq.property/deleted-at 2}
{:keys [properties recycled-only-property-ids]}
(#'property-component/sanitize-property-values-for-display
{:user.property/nodes [recycled-a recycled-b]})]
(is (nil? (:user.property/nodes properties)))
(is (= #{:user.property/nodes}
recycled-only-property-ids))))

View File

@@ -0,0 +1,67 @@
(ns frontend.components.property.value-test
(:require [cljs.test :refer [async deftest is]]
[frontend.components.property.value :as property-value]
[promesa.core :as p]))
(deftest resolve-journal-page-for-date-returns-existing-page-test
(async done
(let [existing-page {:db/id 100
:block/journal-day 20250102}
created?* (atom false)]
(-> (#'property-value/<resolve-journal-page-for-date
(js/Date. "2025-01-02T00:00:00Z")
(constantly "test-repo")
(fn [_repo _title _opts]
(p/resolved existing-page))
(fn [_title _opts]
(reset! created?* true)
(p/resolved {:db/id 999
:block/journal-day 20250102}))
(constantly "Jan 2nd, 2025"))
(p/then (fn [page]
(is (= existing-page page))
(is (false? @created?*))
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest resolve-journal-page-for-date-creates-page-when-missing-test
(async done
(let [created-page {:db/id 200
:block/journal-day 20250102}
created-calls* (atom [])]
(-> (#'property-value/<resolve-journal-page-for-date
(js/Date. "2025-01-02T00:00:00Z")
(constantly "test-repo")
(fn [_repo _title _opts]
(p/resolved nil))
(fn [title opts]
(swap! created-calls* conj [title opts])
(p/resolved created-page))
(constantly "Jan 2nd, 2025"))
(p/then (fn [page]
(is (= created-page page))
(is (= [["Jan 2nd, 2025" {:redirect? false}]] @created-calls*))
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest resolved-property-value-for-render-skips-default-for-placeholder-row-test
(let [property {:db/ident :logseq.property/status
:logseq.property/default-value {:db/id 3
:db/ident :logseq.property/status.todo
:block/title "Todo"}}
placeholder-block {:db/id 1}]
(is (nil? (#'property-value/resolved-property-value-for-render placeholder-block property false)))))
(deftest resolved-property-value-for-render-uses-default-for-loaded-block-test
(let [property {:db/ident :logseq.property/status
:logseq.property/default-value {:db/id 3
:db/ident :logseq.property/status.todo
:block/title "Todo"}}
loaded-block {:db/id 1
:block/uuid #uuid "11111111-1111-1111-1111-111111111111"}]
(is (= (:logseq.property/default-value property)
(#'property-value/resolved-property-value-for-render loaded-block property false)))))

View File

@@ -0,0 +1,67 @@
(ns frontend.config-test
(:require [cljs.test :refer [deftest is testing]]
[frontend.config :as config]))
(deftest custom-url->ws-url-test
(testing "https URL becomes wss"
(is (= "wss://my-server.example.com/sync/%s"
(config/custom-url->ws-url "https://my-server.example.com"))))
(testing "http URL becomes ws"
(is (= "ws://localhost:8787/sync/%s"
(config/custom-url->ws-url "http://localhost:8787"))))
(testing "trailing slashes are stripped"
(is (= "wss://my-server.example.com/sync/%s"
(config/custom-url->ws-url "https://my-server.example.com/")))
(is (= "wss://my-server.example.com/sync/%s"
(config/custom-url->ws-url "https://my-server.example.com///"))))
(testing "preserves port in URL"
(is (= "wss://example.com:3000/sync/%s"
(config/custom-url->ws-url "https://example.com:3000"))))
(testing "preserves subpath in host"
;; Users should only provide a base URL, but verify trailing path doesn't break things
(is (= "wss://example.com/api/sync/%s"
(config/custom-url->ws-url "https://example.com/api")))))
(deftest custom-url->http-base-test
(testing "returns URL as-is when no trailing slash"
(is (= "https://my-server.example.com"
(config/custom-url->http-base "https://my-server.example.com"))))
(testing "strips trailing slashes"
(is (= "https://my-server.example.com"
(config/custom-url->http-base "https://my-server.example.com/")))
(is (= "https://my-server.example.com"
(config/custom-url->http-base "https://my-server.example.com///"))))
(testing "preserves http scheme"
(is (= "http://localhost:8787"
(config/custom-url->http-base "http://localhost:8787"))))
(testing "preserves port"
(is (= "https://example.com:3000"
(config/custom-url->http-base "https://example.com:3000/")))))
(deftest default-urls-are-returned-when-no-custom-url
(testing "db-sync-ws-url returns default when no custom URL is set"
;; In test environment, node-test? is true so get-custom-sync-server-url
;; always returns nil, meaning we always get the default
(is (string? (config/db-sync-ws-url)))
(is (= config/default-db-sync-ws-url (config/db-sync-ws-url))))
(testing "db-sync-http-base returns default when no custom URL is set"
(is (string? (config/db-sync-http-base)))
(is (= config/default-db-sync-http-base (config/db-sync-http-base)))))
(deftest valid-sync-server-url?-test
(testing "accepts http and https URLs"
(is (config/valid-sync-server-url? "https://my-server.example.com"))
(is (config/valid-sync-server-url? "http://localhost:8787")))
(testing "rejects non-URL strings"
(is (not (config/valid-sync-server-url? "not a url")))
(is (not (config/valid-sync-server-url? "")))
(is (not (config/valid-sync-server-url? nil)))))

View File

@@ -86,6 +86,20 @@
(is (= 1 (:db/id (db/entity test-db 1))))
(is (= 1 (:db/id (db/entity (conn/get-db test-db) 1)))))
(deftest get-latest-journals-should-exclude-recycled-journal-pages
(load-test-files
[{:page {:build/journal 20240101}}
{:page {:build/journal 20240102}}
{:page {:build/journal 20240103}}])
(let [journal-2024-01-02-id (ffirst (d/q '[:find ?e
:where
[?e :block/journal-day 20240102]]
(conn/get-db test-db)))]
(db/transact! test-db [{:db/id journal-2024-01-02-id
:logseq.property/deleted-at 1704196800000}]))
(is (= [20240103 20240101]
(mapv :block/journal-day (model/get-latest-journals test-db 10)))))
(deftest get-block-by-page-name-and-block-route-name
(load-test-files
[{:page {:block/title "foo"}

View File

@@ -0,0 +1,46 @@
(ns frontend.db.property-values-test
(:require [cljs.test :refer [deftest is use-fixtures]]
[datascript.core :as d]
[frontend.db :as db]
[frontend.test.helper :as test-helper]
[logseq.db.common.entity-plus :as entity-plus]
[logseq.db.common.view :as db-view]))
(def repo test-helper/test-db)
(defn start-and-destroy-db
[f]
(test-helper/start-and-destroy-db f))
(use-fixtures :each start-and-destroy-db)
(deftest get-property-values-filters-recycled-ref-values-test
(let [property-ident :block/tags
active-title "Active ref value"
recycled-title "Recycled ref value"]
(d/transact! (db/get-db repo false)
[[:db/add -2 :block/title active-title]
[:db/add -3 :block/title recycled-title]
[:db/add -3 :logseq.property/deleted-at 1]
[:db/add -10 property-ident -2]
[:db/add -11 property-ident -3]])
(let [result (db-view/get-property-values @(db/get-db repo false) property-ident {})]
(is (contains? (set (map :label result)) active-title))
(is (not (contains? (set (map :label result)) recycled-title))))))
(deftest property-closed-values-hide-recycled-values-test
(d/transact! (db/get-db repo false)
[{:db/id -1 :db/ident :user.property/closed-values-visibility}
{:db/id -2
:block/title "Visible closed value"
:block/order "a"
:block/closed-value-property -1}
{:db/id -3
:block/title "Recycled closed value"
:block/order "b"
:block/closed-value-property -1
:logseq.property/deleted-at 1}])
(let [db @(db/get-db repo false)
property (d/entity db :user.property/closed-values-visibility)
values (entity-plus/lookup-kv-then-entity property :property/closed-values)]
(is (= ["Visible closed value"] (map :block/title values)))))

View File

@@ -0,0 +1,36 @@
(ns frontend.handler.common.page-test
(:require [clojure.test :refer [async is use-fixtures]]
[datascript.core :as d]
[frontend.db :as db]
[frontend.handler.common.page :as page-common-handler]
[frontend.test.helper :as test-helper :include-macros true :refer [deftest-async]]
[logseq.db :as ldb]
[logseq.db.test.helper :as db-test]
[logseq.outliner.page :as outliner-page]
[promesa.core :as p]))
(use-fixtures :each
{:before (fn []
(async done
(test-helper/start-test-db!)
(done)))
:after test-helper/destroy-test-db!})
(deftest-async create-page-restores-recycled-page
(test-helper/load-test-files [{:page {:block/title "foo"}
:blocks [{:block/title "child block"}]}])
(p/let [conn (db/get-db test-helper/test-db false)
page (db-test/find-page-by-title @conn "foo")
page-uuid (:block/uuid page)
_ (outliner-page/delete! conn page-uuid {})
recycled-page (d/entity @conn [:block/uuid page-uuid])
_ (is (ldb/recycled? recycled-page)
"Page should be recycled after deletion")
restored-page (page-common-handler/<create! "foo" {:redirect? false})]
(is (= (:db/id restored-page) (:db/id page))
"create! should return the restored page")
(let [page' (d/entity @conn [:block/uuid page-uuid])]
(is (not (ldb/recycled? page'))
"Page should no longer be recycled after re-creation")
(is (= "foo" (get-in (db-test/find-block-by-content @conn "child block") [:block/page :block/title]))
"Restored page still has its block(s)"))))

View File

@@ -7,35 +7,8 @@
[frontend.handler.user :as user-handler]
[frontend.state :as state]
[logseq.db :as ldb]
[logseq.db-sync.snapshot :as snapshot]
[promesa.core :as p]))
(defn- encode-datoms-jsonl [datoms]
(snapshot/encode-datoms-jsonl datoms))
(defn- <gzip-bytes [^js payload]
(if (exists? js/CompressionStream)
(p/let [stream (js/ReadableStream.
#js {:start (fn [controller]
(.enqueue controller payload)
(.close controller))})
compressed (.pipeThrough stream (js/CompressionStream. "gzip"))
resp (js/Response. compressed)
buf (.arrayBuffer resp)]
(js/Uint8Array. buf))
(p/resolved payload)))
(defn- bytes->stream
[^js payload chunk-size]
(js/ReadableStream.
#js {:start (fn [controller]
(loop [offset 0]
(when (< offset (.-byteLength payload))
(.enqueue controller (.slice payload offset (min (+ offset chunk-size)
(.-byteLength payload))))
(recur (+ offset chunk-size))))
(.close controller))}))
(deftest remove-member-request-test
(async done
(let [called (atom nil)]
@@ -87,12 +60,18 @@
(deftest rtc-create-graph-persists-disabled-e2ee-flag-test
(async done
(let [fetch-called (atom nil)
tx-called (atom nil)]
tx-called (atom nil)
ensure-calls (atom [])]
(-> (p/with-redefs [db-sync/http-base (fn [] "http://base")
user-handler/task--ensure-id&access-token (fn [resolve _reject]
(resolve true))
db/get-db (fn [] :db)
ldb/get-graph-schema-version (fn [_] {:major 65})
state/<invoke-db-worker (fn [& args]
(when (= :thread-api/db-sync-ensure-user-rsa-keys
(first args))
(swap! ensure-calls conj args))
(p/resolved {:public-key "pk"}))
db-sync/fetch-json (fn [url opts _]
(reset! fetch-called {:url url :opts opts})
(p/resolved {:graph-id "graph-1"
@@ -110,6 +89,9 @@
(is (= "graph-1" graph-id))
(is (= "http://base/graphs" (:url @fetch-called)))
(is (= false (:graph-e2ee? request-body)))
(is (= [[:thread-api/db-sync-ensure-user-rsa-keys
{:ensure-server? true}]]
@ensure-calls))
(is (= :logseq.kv/graph-rtc-e2ee?
(get-in tx-data [2 :db/ident])))
(is (= false
@@ -122,12 +104,18 @@
(deftest rtc-create-graph-defaults-e2ee-enabled-test
(async done
(let [fetch-called (atom nil)
tx-called (atom nil)]
tx-called (atom nil)
ensure-calls (atom [])]
(-> (p/with-redefs [db-sync/http-base (fn [] "http://base")
user-handler/task--ensure-id&access-token (fn [resolve _reject]
(resolve true))
db/get-db (fn [] :db)
ldb/get-graph-schema-version (fn [_] {:major 65})
state/<invoke-db-worker (fn [& args]
(when (= :thread-api/db-sync-ensure-user-rsa-keys
(first args))
(swap! ensure-calls conj args))
(p/resolved {:public-key "pk"}))
db-sync/fetch-json (fn [url opts _]
(reset! fetch-called {:url url :opts opts})
(p/resolved {:graph-id "graph-2"}))
@@ -145,6 +133,9 @@
(is (= "http://base/graphs" (:url @fetch-called)))
(is (= true (:graph-e2ee? request-body)))
(is (= true (:graph-ready-for-use? request-body)))
(is (= [[:thread-api/db-sync-ensure-user-rsa-keys
{:ensure-server? true}]]
@ensure-calls))
(is (= :logseq.kv/graph-rtc-e2ee?
(get-in tx-data [2 :db/ident])))
(is (= true
@@ -189,7 +180,9 @@
js/JSON.parse
(js->clj :keywordize-keys true))]
(is (= false (:graph-ready-for-use? request-body)))
(is (= [[:thread-api/db-sync-upload-graph "logseq_db_demo"]]
(is (= [[:thread-api/db-sync-ensure-user-rsa-keys
{:ensure-server? true}]
[:thread-api/db-sync-upload-graph "logseq_db_demo"]]
@upload-calls))
(is (= 1 @refresh-calls))
(is (= ["logseq_db_demo"] @start-calls))
@@ -309,7 +302,10 @@
(deftest get-remote-graphs-includes-ready-for-use-flag-test
(async done
(let [graphs-state (atom nil)]
(let [graphs-state (atom nil)
worker-prev @state/*db-worker
ensure-calls (atom [])]
(reset! state/*db-worker :worker)
(-> (p/with-redefs [db-sync/http-base (fn [] "http://base")
user-handler/task--ensure-id&access-token (fn [resolve _reject]
(resolve true))
@@ -320,7 +316,13 @@
:graph-e2ee? true
:graph-ready-for-use? false
:created-at 1
:updated-at 2}]}))
:updated-at 2}]
:user-rsa-keys-exists? true}))
state/<invoke-db-worker (fn [& args]
(when (= :thread-api/db-sync-ensure-user-rsa-keys
(first args))
(swap! ensure-calls conj args))
(p/resolved :ok))
state/set-state! (fn [k v]
(when (= k :rtc/graphs)
(reset! graphs-state v))
@@ -330,159 +332,90 @@
(p/then (fn [graphs]
(is (= false (:graph-ready-for-use? (first graphs))))
(is (= false (:graph-ready-for-use? (first @graphs-state))))
(is (empty? @ensure-calls))
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest rtc-download-graph-imports-snapshot-once-test
(async done
(let [import-calls (atom [])
fetch-calls (atom [])
datoms [{:e 1 :a :db/ident :v :logseq.class/Page :tx 1 :added true}
{:e 2 :a :block/title :v "hello" :tx 1 :added true}]
jsonl-bytes (encode-datoms-jsonl datoms)
original-fetch js/fetch
download-url "http://base/sync/graph-1/snapshot/download"
asset-url "http://base/assets/graph-1/snapshot-1.snapshot"]
(-> (p/let [gzip-bytes (<gzip-bytes jsonl-bytes)]
(set! js/fetch
(fn [url opts]
(let [method (or (aget opts "method") "GET")]
(swap! fetch-calls conj [url method])
(cond
(and (= url asset-url) (= method "GET"))
(js/Promise.resolve
#js {:ok true
:status 200
:headers #js {:get (fn [header]
(when (= header "content-length")
(str (.-byteLength gzip-bytes))))}
:arrayBuffer (fn [] (js/Promise.resolve (.-buffer gzip-bytes)))})
:else
(js/Promise.resolve
#js {:ok true
:status 200})))))
(-> (p/with-redefs [db-sync/http-base (fn [] "http://base")
db-sync/fetch-json (fn [url _opts _schema]
(cond
(string/ends-with? url "/pull")
(p/resolved {:t 42})
(= url download-url)
(p/resolved {:ok true
:url asset-url
:key "graph-1/snapshot-1.snapshot"
:content-encoding "gzip"})
:else
(p/rejected (ex-info "unexpected fetch-json URL"
{:url url}))))
user-handler/task--ensure-id&access-token (fn [resolve _reject]
(resolve true))
state/<invoke-db-worker (fn [& args]
(swap! import-calls conj args)
(if (= :thread-api/db-sync-import-prepare (first args))
(p/resolved {:import-id "import-1"})
(p/resolved :ok)))
state/set-state! (fn [& _] nil)
state/pub-event! (fn [& _] nil)]
(db-sync/<rtc-download-graph! "demo-graph" "graph-1" false))
(p/finally (fn [] (set! js/fetch original-fetch)))))
(p/then (fn [_]
(is (= 3 (count @import-calls)))
(let [[prepare-op graph reset? graph-uuid graph-e2ee?] (first @import-calls)
[chunk-op imported-datoms chunk-graph-uuid import-id] (second @import-calls)
[finalize-op finalize-graph finalize-graph-uuid remote-tx finalize-import-id] (nth @import-calls 2)]
(is (= :thread-api/db-sync-import-prepare prepare-op))
(is (string/ends-with? graph "demo-graph"))
(is (= true reset?))
(is (= "graph-1" graph-uuid))
(is (= false graph-e2ee?))
(is (= :thread-api/db-sync-import-datoms-chunk chunk-op))
(is (= datoms imported-datoms))
(is (= "graph-1" chunk-graph-uuid))
(is (= "import-1" import-id))
(is (= :thread-api/db-sync-import-finalize finalize-op))
(is (string/ends-with? finalize-graph "demo-graph"))
(is (= "graph-1" finalize-graph-uuid))
(is (= 42 remote-tx))
(is (= "import-1" finalize-import-id)))
(is (= [[asset-url "GET"]]
@fetch-calls))
(done)))
(p/catch (fn [error]
(set! js/fetch original-fetch)
(is false (str error))
(done)))))))
(deftest rtc-download-graph-streams-gzip-snapshot-test
(async done
(let [import-calls (atom [])
datoms [{:e 1 :a :db/ident :v :logseq.class/Page :tx 1 :added true}
{:e 2 :a :block/title :v "hello" :tx 1 :added true}]
jsonl-bytes (encode-datoms-jsonl datoms)
original-fetch js/fetch
download-url "http://base/sync/graph-1/snapshot/download"
asset-url "http://base/assets/graph-1/snapshot-1.snapshot"
worker-prev @state/*db-worker]
(reset! state/*db-worker nil)
(-> (p/let [gzip-bytes (<gzip-bytes jsonl-bytes)
stream (bytes->stream gzip-bytes 3)]
(set! js/fetch
(fn [url opts]
(let [method (or (aget opts "method") "GET")]
(cond
(and (= url asset-url) (= method "GET"))
(js/Promise.resolve
#js {:ok true
:status 200
:headers #js {:get (fn [header]
(case header
"content-length" (str (.-byteLength gzip-bytes))
"content-encoding" "gzip"
nil))}
:body stream
:arrayBuffer (fn [] (throw (js/Error. "arrayBuffer should not be used")))})
:else
(js/Promise.resolve #js {:ok false :status 404})))))
(-> (p/with-redefs [db-sync/http-base (fn [] "http://base")
db-sync/fetch-json (fn [url _opts _schema]
(cond
(string/ends-with? url "/pull")
(p/resolved {:t 42})
(= url download-url)
(p/resolved {:ok true
:url asset-url
:key "graph-1/snapshot-1.snapshot"
:content-encoding "gzip"})
:else
(p/rejected (ex-info "unexpected fetch-json URL"
{:url url}))))
user-handler/task--ensure-id&access-token (fn [resolve _reject]
(resolve true))
state/<invoke-db-worker (fn [& args]
(swap! import-calls conj args)
(if (= :thread-api/db-sync-import-prepare (first args))
(p/resolved {:import-id "import-1"})
(p/resolved :ok)))
state/set-state! (fn [& _] nil)
state/pub-event! (fn [& _] nil)]
(db-sync/<rtc-download-graph! "demo-graph" "graph-1" false))
(p/finally (fn [] (set! js/fetch original-fetch)))))
(p/then (fn [_]
(is (= 3 (count @import-calls)))
(let [[chunk-op imported-datoms _ import-id] (second @import-calls)]
(is (= :thread-api/db-sync-import-datoms-chunk chunk-op))
(is (= datoms imported-datoms))
(is (= "import-1" import-id)))
(done)))
(p/catch (fn [error]
(reset! state/*db-worker worker-prev)
(set! js/fetch original-fetch)
(is false (str error))
(done)))
(p/finally (fn []
(reset! state/*db-worker worker-prev)))))))
(deftest get-remote-graphs-ensures-user-rsa-keys-when-server-missing-test
(async done
(let [worker-prev @state/*db-worker
ensure-calls (atom [])]
(reset! state/*db-worker :worker)
(-> (p/with-redefs [db-sync/http-base (fn [] "http://base")
user-handler/task--ensure-id&access-token (fn [resolve _reject]
(resolve true))
db-sync/fetch-json (fn [_url _opts _schema]
(p/resolved {:graphs []
:user-rsa-keys-exists? false}))
state/<invoke-db-worker (fn [& args]
(when (= :thread-api/db-sync-ensure-user-rsa-keys
(first args))
(swap! ensure-calls conj args))
(p/resolved {:public-key "pk"}))
state/set-state! (fn [& _] nil)
repo-handler/refresh-repos! (fn [] nil)]
(db-sync/<get-remote-graphs))
(p/then (fn [_]
(is (= [[:thread-api/db-sync-ensure-user-rsa-keys
{:ensure-server? true
:server-rsa-keys-exists? false}]]
@ensure-calls))
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))
(p/finally (fn []
(reset! state/*db-worker worker-prev)))))))
(deftest rtc-download-graph-delegates-to-worker-download-api-test
(async done
(let [worker-calls (atom [])]
(-> (p/with-redefs [db-sync/http-base (fn [] "http://base")
user-handler/task--ensure-id&access-token (fn [resolve _reject]
(resolve true))
state/<invoke-db-worker (fn [& args]
(swap! worker-calls conj args)
(p/resolved :ok))
state/set-state! (fn [& _] nil)
state/pub-event! (fn [& _] nil)]
(db-sync/<rtc-download-graph! "demo-graph" "graph-1" false))
(p/then (fn [_]
(is (= 1 (count @worker-calls)))
(let [[op graph graph-uuid graph-e2ee?] (first @worker-calls)]
(is (= :thread-api/db-sync-download-graph op))
(is (string/ends-with? graph "demo-graph"))
(is (= "graph-1" graph-uuid))
(is (= false graph-e2ee?)))
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest rtc-download-graph-sets-and-clears-downloading-state-test
(async done
(let [state-calls (atom [])
worker-prev @state/*db-worker]
(reset! state/*db-worker nil)
(-> (p/with-redefs [db-sync/http-base (fn [] "http://base")
user-handler/task--ensure-id&access-token (fn [resolve _reject]
(resolve true))
state/<invoke-db-worker (fn [& _] (p/resolved :ok))
state/pub-event! (fn [& _] nil)
state/set-state! (fn [k v]
(swap! state-calls conj [k v])
nil)]
(db-sync/<rtc-download-graph! "demo-graph" "graph-1" false))
(p/then (fn [_]
(is (= [[:rtc/downloading-graph-uuid "graph-1"]
[:rtc/downloading-graph-uuid nil]]
@state-calls))
(done)))
(p/catch (fn [error]
(reset! state/*db-worker worker-prev)
(is false (str error))
(done)))
(p/finally (fn []

View File

@@ -74,14 +74,13 @@
[(missing? $ ?b :logseq.property/deleted-at)]]
@conn)
(map (comp :block/title first)))
recycled-blocks (->> (d/q '[:find (pull ?b [*])
:where
[?b :logseq.property/deleted-at]
[?b :block/title ""]]
@conn)
(map first))]
deleted-blocks (->> (d/q '[:find (pull ?b [*])
:where
[?b :block/title ""]]
@conn)
(map first))]
(is (= ["b1" "b2"] updated-blocks) "Visible page blocks stay on the page")
(is (= 1 (count recycled-blocks)) "Deleted block moves to recycle")))})))
(is (empty? deleted-blocks) "Deleted block is removed from page db")))})))
(testing "backspace deletes empty block in embedded context"
;; testing embed at this layer doesn't require an embed block since
@@ -104,11 +103,10 @@
[(missing? $ ?b :logseq.property/deleted-at)]]
@conn)
(map (comp :block/title first)))
recycled-blocks (->> (d/q '[:find (pull ?b [*])
:where
[?b :logseq.property/deleted-at]
[?b :block/title ""]]
@conn)
(map first))]
deleted-blocks (->> (d/q '[:find (pull ?b [*])
:where
[?b :block/title ""]]
@conn)
(map first))]
(is (= ["b1" "b2"] updated-blocks) "Visible page blocks stay on the page")
(is (= 1 (count recycled-blocks)) "Deleted block moves to recycle")))}))))
(is (empty? deleted-blocks) "Deleted block is removed from page db")))}))))

View File

@@ -0,0 +1,107 @@
(ns frontend.handler.history-test
(:require [clojure.test :refer [deftest is]]
[frontend.db :as db]
[frontend.handler.editor :as editor]
[frontend.handler.history :as history]
[frontend.state :as state]
[frontend.util :as util]
[logseq.db :as ldb]))
(deftest restore-cursor-and-state-prefers-ui-state-test
(let [pause-calls (atom [])
app-state-calls (atom [])
cursor-calls (atom [])]
(with-redefs [state/set-state! (fn [k v]
(swap! pause-calls conj [k v]))
ldb/read-transit-str (fn [_]
{:old-state {:route-data {:to :page}}
:new-state {:route-data {:to :home}}})
history/restore-app-state! (fn [app-state]
(swap! app-state-calls conj app-state))
history/restore-cursor! (fn [data]
(swap! cursor-calls conj data))]
(#'history/restore-cursor-and-state!
{:ui-state-str "ui-state"
:undo? true
:editor-cursors [{:block-uuid (random-uuid)}]})
(is (= [[:history/paused? true]
[:history/paused? false]]
@pause-calls))
(is (= [{:route-data {:to :page}}]
@app-state-calls))
(is (empty? @cursor-calls)))))
(deftest restore-cursor-and-state-falls-back-to-cursor-test
(let [pause-calls (atom [])
app-state-calls (atom [])
cursor-calls (atom [])]
(with-redefs [state/set-state! (fn [k v]
(swap! pause-calls conj [k v]))
history/restore-app-state! (fn [app-state]
(swap! app-state-calls conj app-state))
history/restore-cursor! (fn [data]
(swap! cursor-calls conj data))]
(#'history/restore-cursor-and-state!
{:ui-state-str nil
:undo? false
:editor-cursors [{:block-uuid (random-uuid)
:start-pos 1
:end-pos 2}]})
(is (= [[:history/paused? true]
[:history/paused? false]]
@pause-calls))
(is (empty? @app-state-calls))
(is (= 1 (count @cursor-calls)))
(is (nil? (:ui-state-str (first @cursor-calls))))
(is (= false (:undo? (first @cursor-calls)))))))
(deftest restore-cursor-prefers-block-selection-test
(let [selection-calls (atom [])
edit-calls (atom [])]
(with-redefs [util/get-blocks-by-id (fn [block-id]
(case block-id
#uuid "00000000-0000-0000-0000-000000000001" [:node-1]
#uuid "00000000-0000-0000-0000-000000000002" [:node-2]
nil))
state/exit-editing-and-set-selected-blocks! (fn [blocks direction]
(swap! selection-calls conj [blocks direction]))
editor/edit-block! (fn [& args]
(swap! edit-calls conj args))
db/pull (constantly nil)]
(#'history/restore-cursor!
{:undo? true
:editor-cursors [{:selected-block-uuids [#uuid "00000000-0000-0000-0000-000000000001"
#uuid "00000000-0000-0000-0000-000000000002"]
:selection-direction :down}]})
(is (= [[[:node-1 :node-2] :down]]
@selection-calls))
(is (empty? @edit-calls)))))
(deftest restore-cursor-selection-falls-back-to-editor-cursor-test
(let [selection-calls (atom [])
edit-calls (atom [])
block-uuid #uuid "00000000-0000-0000-0000-000000000003"]
(with-redefs [util/get-blocks-by-id (constantly nil)
state/exit-editing-and-set-selected-blocks! (fn [blocks direction]
(swap! selection-calls conj [blocks direction]))
editor/edit-block! (fn [& args]
(swap! edit-calls conj args))
db/pull (fn [[_lookup-k id]]
(when (= block-uuid id)
{:db/id 42
:block/uuid block-uuid}))]
(#'history/restore-cursor!
{:undo? false
:editor-cursors [{:selected-block-uuids [#uuid "00000000-0000-0000-0000-000000000001"]
:selection-direction :up
:block-uuid block-uuid
:container-id 99
:start-pos 1
:end-pos 3}]})
(is (empty? @selection-calls))
(is (= [[{:db/id 42
:block/uuid block-uuid}
3
{:container-id 99
:custom-content nil}]]
@edit-calls)))))

View File

@@ -0,0 +1,70 @@
(ns frontend.handler.user-test
(:require [cljs.test :refer [deftest is testing]]
[frontend.handler.user :as user-handler]
[frontend.state :as state]
[promesa.core :as p]))
(defn- with-mocked-local-storage
[f]
(let [old-storage (.-localStorage js/globalThis)
had-local-storage?
(.call (.-hasOwnProperty (.-prototype js/Object))
js/globalThis
"localStorage")
mocked-storage #js {:clear (fn [] nil)
:setItem (fn [& _] nil)
:getItem (fn [& _] nil)
:removeItem (fn [& _] nil)}]
(js/Object.defineProperty js/globalThis
"localStorage"
#js {:value mocked-storage
:configurable true
:writable true})
(try
(f)
(finally
(if had-local-storage?
(js/Object.defineProperty js/globalThis
"localStorage"
#js {:value old-storage
:configurable true
:writable true})
(js/Reflect.deleteProperty js/globalThis "localStorage"))))))
(deftest logout-clears-e2ee-password-when-db-worker-ready-test
(testing "logout should request db-worker to clear persisted e2ee password"
(let [ops* (atom [])
old-worker @state/*db-worker]
(reset! state/*db-worker :worker)
(try
(with-mocked-local-storage
(fn []
(with-redefs [state/<invoke-db-worker (fn [op & _]
(swap! ops* conj op)
(p/resolved nil))
state/clear-user-info! (fn [] nil)
state/pub-event! (fn [& _] nil)
user-handler/clear-tokens (fn [] nil)]
(user-handler/logout)
(is (= [:thread-api/clear-e2ee-password] @ops*)))))
(finally
(reset! state/*db-worker old-worker))))))
(deftest logout-skips-e2ee-password-clear-when-db-worker-missing-test
(testing "logout should not call db-worker API when db-worker is unavailable"
(let [invoke-calls* (atom 0)
old-worker @state/*db-worker]
(reset! state/*db-worker nil)
(try
(with-mocked-local-storage
(fn []
(with-redefs [state/<invoke-db-worker (fn [& _]
(swap! invoke-calls* inc)
(p/resolved nil))
state/clear-user-info! (fn [] nil)
state/pub-event! (fn [& _] nil)
user-handler/clear-tokens (fn [] nil)]
(user-handler/logout)
(is (zero? @invoke-calls*)))))
(finally
(reset! state/*db-worker old-worker))))))

View File

@@ -40,7 +40,9 @@
#(test-helper/start-and-destroy-db
%
{:build-init-data? false
:schema {:logseq.property/deleted-at {:db/index true}}})
:schema {:logseq.property/deleted-at {:db/index true}
:logseq.property/created-from-property {:db/index true}
}})
listen-db-fixture)
(defn get-block
@@ -530,11 +532,7 @@
(is (= [5] (get-children 4)))
(let [recycled (get-block 3)
recycle-page (db/get-page "Recycle")]
(is (some? recycled))
(is (integer? (:logseq.property/deleted-at recycled)))
(is (= (:db/id recycle-page) (:db/id (:block/page recycled)))))))))
(is (nil? (get-block 3)))))))
(deftest test-bocks-with-level
(testing "blocks with level"

View File

@@ -24,3 +24,18 @@
{:shortcuts {:ui/toggle-brackets "t b"}}
{:shortcuts {:editor/up ["ctrl+p" "up"]}}))
"Map values get merged across configs"))
(deftest get-editor-info-includes-selection-when-not-editing-test
(let [selected-ids [(random-uuid) (random-uuid)]]
(with-redefs [state/get-edit-block (constantly nil)
state/get-selection-block-ids (constantly selected-ids)
state/get-selection-direction (constantly :down)]
(is (= {:selected-block-uuids selected-ids
:selection-direction :down}
(state/get-editor-info))))))
(deftest get-editor-info-returns-nil-when-not-editing-and-no-selection-test
(with-redefs [state/get-edit-block (constantly nil)
state/get-selection-block-ids (constantly nil)
state/get-selection-direction (constantly nil)]
(is (nil? (state/get-editor-info)))))

View File

@@ -1,672 +1,74 @@
(ns frontend.undo-redo-test
(:require [clojure.test :as t :refer [deftest is testing use-fixtures]]
[datascript.core :as d]
[frontend.db :as db]
[frontend.handler.editor :as editor]
[frontend.modules.outliner.core-test :as outliner-test]
(:require [clojure.test :refer [deftest is]]
[frontend.state :as state]
[frontend.test.helper :as test-helper]
[frontend.undo-redo :as undo-redo]
[frontend.worker.db-listener :as worker-db-listener]
[logseq.db :as ldb]
[logseq.outliner.core :as outliner-core]
[logseq.outliner.op :as outliner-op]
[logseq.undo-redo-validate :as undo-validate]))
[frontend.util :as util]))
;; TODO: random property ops test
;; ADR 0013 note: this namespace keeps main-thread coordination coverage only.
;; Worker-owned DB-history recording/replay tests belong under src/test/frontend/worker/.
(def test-db test-helper/test-db)
(deftest undo-redo-proxy-to-worker-test
(let [calls (atom [])
invoke! (fn [& args]
(swap! calls conj (vec args))
(vec args))
repo "repo-1"]
(with-redefs [util/node-test? false
state/<invoke-db-worker invoke!]
(is (= [:thread-api/undo-redo-undo repo]
(undo-redo/undo repo)))
(is (= [:thread-api/undo-redo-redo repo]
(undo-redo/redo repo)))
(is (= [[:thread-api/undo-redo-undo repo]
[:thread-api/undo-redo-redo repo]]
@calls)))))
(defmethod worker-db-listener/listen-db-changes :gen-undo-ops
[_ {:keys [repo]} tx-report]
(undo-redo/gen-undo-ops! repo
(-> tx-report
(assoc-in [:tx-meta :client-id] (:client-id @state/state))
(update-in [:tx-meta :local-tx?] (fn [local-tx?]
(if (nil? local-tx?)
true
local-tx?))))))
(deftest clear-history-and-record-editor-info-proxy-test
(let [calls (atom [])
invoke! (fn [& args]
(swap! calls conj (vec args))
(vec args))
repo "repo-2"
editor-info {:block-uuid (random-uuid)
:container-id 1
:start-pos 0
:end-pos 3}]
(with-redefs [util/node-test? false
state/<invoke-db-worker invoke!]
(is (= [:thread-api/undo-redo-clear-history repo]
(undo-redo/clear-history! repo)))
(is (= [:thread-api/undo-redo-record-editor-info repo editor-info]
(undo-redo/record-editor-info! repo editor-info)))
(is (= [[:thread-api/undo-redo-clear-history repo]
[:thread-api/undo-redo-record-editor-info repo editor-info]]
@calls)))))
(defn listen-db-fixture
[f]
(let [test-db-conn (db/get-db test-db false)]
(assert (some? test-db-conn))
(worker-db-listener/listen-db-changes! test-db test-db-conn
{:handler-keys [:gen-undo-ops]})
(f)
(d/unlisten! test-db-conn :frontend.worker.db-listener/listen-db-changes!)))
(deftest record-ui-state-proxy-test
(let [calls (atom [])
invoke! (fn [& args]
(swap! calls conj (vec args))
(vec args))
repo "repo-3"
ui-state-str "{:old-state {}, :new-state {:route-data {:to :page}}}"]
(with-redefs [util/node-test? false
state/<invoke-db-worker invoke!]
(is (nil? (undo-redo/record-ui-state! repo nil)))
(is (= [:thread-api/undo-redo-record-ui-state repo ui-state-str]
(undo-redo/record-ui-state! repo ui-state-str)))
(is (= [[:thread-api/undo-redo-record-ui-state repo ui-state-str]]
@calls)))))
(defn disable-browser-fns
[f]
;; get-selection-blocks has a js/document reference
(with-redefs [state/get-selection-blocks (constantly [])]
(f)))
(defn with-worker-undo-validation
[f]
(let [orig-transact ldb/transact!]
(with-redefs [ldb/transact! (fn [repo-or-conn tx-data tx-meta]
(if (and (or (:undo? tx-meta) (:redo? tx-meta))
(not (undo-validate/valid-undo-redo-tx? repo-or-conn tx-data)))
(throw (ex-info "undo/redo tx invalid"
{:undo? (:undo? tx-meta)
:redo? (:redo? tx-meta)}))
(if (satisfies? IDeref repo-or-conn)
(d/transact! repo-or-conn tx-data tx-meta)
(orig-transact repo-or-conn tx-data tx-meta))))]
(f))))
(use-fixtures :each
disable-browser-fns
with-worker-undo-validation
test-helper/react-components
#(test-helper/start-and-destroy-db % {:build-init-data? false
:schema {:logseq.property/deleted-at {:db/index true}}})
listen-db-fixture)
(defn- undo-all!
[]
(loop [i 0]
(let [r (undo-redo/undo test-db)]
(if (not= :frontend.undo-redo/empty-undo-stack r)
(recur (inc i))
(prn :undo-count i)))))
(defn- redo-all!
[]
(loop [i 0]
(let [r (undo-redo/redo test-db)]
(if (not= :frontend.undo-redo/empty-redo-stack r)
(recur (inc i))
(prn :redo-count i)))))
(defn- parent-cycle?
[ent]
(let [start (:block/uuid ent)]
(loop [current ent
seen #{start}
steps 0]
(cond
(>= steps 200) true
(nil? (:block/parent current)) false
:else (let [next-ent (:block/parent current)
next-uuid (:block/uuid next-ent)]
(if (contains? seen next-uuid)
true
(recur next-ent (conj seen next-uuid) (inc steps))))))))
(defn- db-issues
[db]
(let [ignore-ent? (fn [ent]
(or (ldb/recycled? ent)
(= "Recycle" (:block/title ent))
(= "Recycle" (some-> ent :block/page :block/title))))
ents (->> (d/q '[:find [?e ...]
:where
[?e :block/uuid]]
db)
(map (fn [e] (d/entity db e)))
(remove ignore-ent?))
uuid-required-ids (->> (concat
(d/q '[:find [?e ...]
:where
[?e :block/title]]
db)
(d/q '[:find [?e ...]
:where
[?e :block/page]]
db)
(d/q '[:find [?e ...]
:where
[?e :block/parent]]
db))
distinct)]
(concat
(for [e uuid-required-ids
:let [ent (d/entity db e)]
:when (and (not (ignore-ent? ent))
(nil? (:block/uuid ent)))]
{:type :missing-uuid :e e})
(for [ent ents
:let [uuid (:block/uuid ent)
parent (:block/parent ent)]
:when (and (not (ldb/page? ent)) (nil? parent))]
{:type :missing-parent :uuid uuid})
(for [ent ents
:let [uuid (:block/uuid ent)
parent (:block/parent ent)]
:when (and (not (ldb/page? ent)) parent (nil? (:block/uuid parent)))]
{:type :missing-parent-ref :uuid uuid})
(for [ent ents
:let [uuid (:block/uuid ent)
page (:block/page ent)]
:when (and (not (ldb/page? ent)) (nil? page))]
{:type :missing-page :uuid uuid})
(for [ent ents
:let [uuid (:block/uuid ent)
page (:block/page ent)]
:when (and (not (ldb/page? ent)) page (not (ldb/page? page)))]
{:type :page-not-page :uuid uuid})
(for [ent ents
:let [uuid (:block/uuid ent)
parent (:block/parent ent)
page (:block/page ent)
expected-page (when parent
(if (ldb/page? parent) parent (:block/page parent)))]
:when (and (not (ldb/page? ent))
parent
page
expected-page
(not= (:block/uuid expected-page) (:block/uuid page)))]
{:type :page-mismatch :uuid uuid})
(for [ent ents
:let [uuid (:block/uuid ent)
parent (:block/parent ent)]
:when (and parent (= uuid (:block/uuid parent)))]
{:type :self-parent :uuid uuid})
(for [ent ents
:let [uuid (:block/uuid ent)]
:when (and (not (ldb/page? ent))
(parent-cycle? ent))]
{:type :cycle :uuid uuid}))))
(defn- seed-page-parent-child!
[]
(let [conn (db/get-db test-db false)
page-uuid (random-uuid)
parent-uuid (random-uuid)
child-uuid (random-uuid)]
(d/transact! conn
[{:db/ident :logseq.class/Page}
{:block/uuid page-uuid
:block/name "page"
:block/title "page"
:block/tags #{:logseq.class/Page}}
{:block/uuid parent-uuid
:block/title "parent"
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid page-uuid]}
{:block/uuid child-uuid
:block/title "child"
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid parent-uuid]}]
{:outliner-op :insert-blocks
:local-tx? false})
{:page-uuid page-uuid
:parent-uuid parent-uuid
:child-uuid child-uuid}))
(defn- seed-page-two-parents-child!
[]
(let [conn (db/get-db test-db false)
page-uuid (random-uuid)
parent-a-uuid (random-uuid)
parent-b-uuid (random-uuid)
child-uuid (random-uuid)]
(d/transact! conn
[{:db/ident :logseq.class/Page}
{:block/uuid page-uuid
:block/name "page"
:block/title "page"
:block/tags #{:logseq.class/Page}}
{:block/uuid parent-a-uuid
:block/title "parent-a"
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid page-uuid]}
{:block/uuid parent-b-uuid
:block/title "parent-b"
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid page-uuid]}
{:block/uuid child-uuid
:block/title "child"
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid parent-a-uuid]}]
{:outliner-op :insert-blocks
:local-tx? false})
{:page-uuid page-uuid
:parent-a-uuid parent-a-uuid
:parent-b-uuid parent-b-uuid
:child-uuid child-uuid}))
(deftest undo-records-only-local-txs-test
(testing "undo history records only local txs"
(undo-redo/clear-history! test-db)
(let [conn (db/get-db test-db false)
{:keys [child-uuid]} (seed-page-parent-child!)]
(d/transact! conn
[[:db/add [:block/uuid child-uuid] :block/title "local-update"]]
{:outliner-op :save-block
:local-tx? true})
(let [undo-result (undo-redo/undo test-db)]
(is (not= :frontend.undo-redo/empty-undo-stack undo-result))
(undo-redo/redo test-db)))
(undo-redo/clear-history! test-db)
(let [conn (db/get-db test-db false)
{:keys [child-uuid]} (seed-page-parent-child!)]
(d/transact! conn
[[:db/add [:block/uuid child-uuid] :block/title "remote-update"]]
{:outliner-op :save-block
:local-tx? false})
(is (= :frontend.undo-redo/empty-undo-stack (undo-redo/undo test-db))))))
(deftest single-op-apply-ops-preserves-local-tx-and-client-id-test
(testing "single local outliner ops should reach listeners with local/client metadata intact"
(let [conn (db/get-db test-db false)
{:keys [child-uuid]} (seed-page-parent-child!)
tx-meta* (atom nil)]
(d/listen! conn ::capture-tx-meta
(fn [{:keys [tx-meta]}]
(reset! tx-meta* tx-meta)))
(try
(outliner-op/apply-ops! conn
[[:save-block [{:block/uuid child-uuid
:block/title "single-op-save"} {}]]]
{:client-id (:client-id @state/state)
:local-tx? true})
(is (= true (:local-tx? @tx-meta*)))
(is (= (:client-id @state/state) (:client-id @tx-meta*)))
(finally
(d/unlisten! conn ::capture-tx-meta))))))
(deftest undo-conflict-clears-history-test
(testing "undo clears history when reverse tx is unsafe"
(undo-redo/clear-history! test-db)
(let [conn (db/get-db test-db false)
block-uuid (random-uuid)]
(d/transact! conn [{:block/uuid block-uuid
:block/title "conflict"}]
{:outliner-op :insert-blocks
:local-tx? true})
(with-redefs [undo-redo/get-reversed-datoms (fn [& _] nil)]
(is (= :frontend.undo-redo/empty-undo-stack (undo-redo/undo test-db)))))))
(deftest undo-works-for-local-graph-test
(testing "undo/redo works for local changes on local graph"
(undo-redo/clear-history! test-db)
(let [conn (db/get-db test-db false)
{:keys [child-uuid]} (seed-page-parent-child!)]
(d/transact! conn
[[:db/add [:block/uuid child-uuid] :block/title "local-1"]]
{:outliner-op :save-block
:local-tx? true})
(let [undo-result (undo-redo/undo test-db)]
(is (not= :frontend.undo-redo/empty-undo-stack undo-result))
(is (= "child" (:block/title (d/entity @conn [:block/uuid child-uuid])))))
(let [redo-result (undo-redo/redo test-db)]
(is (not= :frontend.undo-redo/empty-redo-stack redo-result))
(is (= "local-1" (:block/title (d/entity @conn [:block/uuid child-uuid]))))))))
(deftest undo-insert-retracts-added-entity-cleanly-test
(testing "undoing a local insert retracts the inserted entity instead of leaving a partial shell"
(undo-redo/clear-history! test-db)
(let [conn (db/get-db test-db false)
{:keys [page-uuid]} (seed-page-parent-child!)
inserted-uuid (random-uuid)]
(d/transact! conn
[{:block/uuid inserted-uuid
:block/title "inserted"
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid page-uuid]}]
{:outliner-op :insert-blocks
:local-tx? true})
(is (some? (d/entity @conn [:block/uuid inserted-uuid])))
(let [undo-result (undo-redo/undo test-db)]
(is (not= :frontend.undo-redo/empty-undo-stack undo-result))
(is (nil? (d/entity @conn [:block/uuid inserted-uuid])))))))
(deftest repeated-save-block-content-undo-redo-test
(testing "multiple saves on the same block undo and redo one step at a time"
(undo-redo/clear-history! test-db)
(let [conn (db/get-db test-db false)
{:keys [child-uuid]} (seed-page-parent-child!)]
(doseq [title ["v1" "v2" "v3"]]
(d/transact! conn
[[:db/add [:block/uuid child-uuid] :block/title title]]
{:outliner-op :save-block
:local-tx? true}))
(is (= "v3" (:block/title (d/entity @conn [:block/uuid child-uuid]))))
(undo-redo/undo test-db)
(is (= "v2" (:block/title (d/entity @conn [:block/uuid child-uuid]))))
(undo-redo/undo test-db)
(is (= "v1" (:block/title (d/entity @conn [:block/uuid child-uuid]))))
(undo-redo/undo test-db)
(is (= "child" (:block/title (d/entity @conn [:block/uuid child-uuid]))))
(undo-redo/redo test-db)
(is (= "v1" (:block/title (d/entity @conn [:block/uuid child-uuid]))))
(undo-redo/redo test-db)
(is (= "v2" (:block/title (d/entity @conn [:block/uuid child-uuid]))))
(undo-redo/redo test-db)
(is (= "v3" (:block/title (d/entity @conn [:block/uuid child-uuid])))))))
(deftest repeated-editor-save-block-content-undo-redo-test
(testing "editor/save-block! records sequential content saves in order"
(undo-redo/clear-history! test-db)
(let [conn (db/get-db test-db false)
{:keys [child-uuid]} (seed-page-parent-child!)]
(doseq [title ["foo" "foo bar"]]
(editor/save-block! test-db child-uuid title))
(is (= "foo bar" (:block/title (d/entity @conn [:block/uuid child-uuid]))))
(undo-redo/undo test-db)
(is (= "foo" (:block/title (d/entity @conn [:block/uuid child-uuid]))))
(undo-redo/redo test-db)
(is (= "foo bar" (:block/title (d/entity @conn [:block/uuid child-uuid])))))))
(deftest editor-save-two-blocks-undo-targets-latest-block-test
(testing "undo after saving two different blocks reverts the latest saved block first"
(undo-redo/clear-history! test-db)
(let [conn (db/get-db test-db false)
{:keys [parent-uuid child-uuid]} (seed-page-parent-child!)]
(editor/save-block! test-db parent-uuid "parent updated")
(editor/save-block! test-db child-uuid "child updated")
(undo-redo/undo test-db)
(is (= "parent updated" (:block/title (d/entity @conn [:block/uuid parent-uuid]))))
(is (= "child" (:block/title (d/entity @conn [:block/uuid child-uuid]))))
(undo-redo/undo test-db)
(is (= "parent" (:block/title (d/entity @conn [:block/uuid parent-uuid])))))))
(deftest new-local-save-clears-redo-stack-test
(testing "a new local save clears redo history"
(undo-redo/clear-history! test-db)
(let [conn (db/get-db test-db false)
{:keys [child-uuid]} (seed-page-parent-child!)]
(editor/save-block! test-db child-uuid "v1")
(editor/save-block! test-db child-uuid "v2")
(undo-redo/undo test-db)
(is (= "v1" (:block/title (d/entity @conn [:block/uuid child-uuid]))))
(editor/save-block! test-db child-uuid "v3")
(is (= :frontend.undo-redo/empty-redo-stack (undo-redo/redo test-db)))
(is (= "v3" (:block/title (d/entity @conn [:block/uuid child-uuid])))))))
(deftest insert-save-delete-sequence-undo-redo-test
(testing "insert then save then recycle-delete can be undone and redone in order"
(undo-redo/clear-history! test-db)
(let [conn (db/get-db test-db false)
{:keys [page-uuid]} (seed-page-parent-child!)
inserted-uuid (random-uuid)
recycle-title "Recycle"]
(d/transact! conn
[{:block/uuid inserted-uuid
:block/title "draft"
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid page-uuid]}]
{:outliner-op :insert-blocks
:local-tx? true})
(d/transact! conn
[[:db/add [:block/uuid inserted-uuid] :block/title "published"]]
{:outliner-op :save-block
:local-tx? true})
(outliner-core/delete-blocks! conn [(d/entity @conn [:block/uuid inserted-uuid])] {})
(is (= recycle-title
(:block/title (:block/page (d/entity @conn [:block/uuid inserted-uuid])))))
(undo-redo/undo test-db)
(let [restored (d/entity @conn [:block/uuid inserted-uuid])]
(is (= page-uuid (:block/uuid (:block/page restored))))
(is (= "published" (:block/title restored))))
(undo-redo/undo test-db)
(is (= "draft" (:block/title (d/entity @conn [:block/uuid inserted-uuid]))))
(undo-redo/undo test-db)
(is (nil? (d/entity @conn [:block/uuid inserted-uuid])))
(undo-redo/redo test-db)
(is (= "draft" (:block/title (d/entity @conn [:block/uuid inserted-uuid]))))
(undo-redo/redo test-db)
(is (= "published" (:block/title (d/entity @conn [:block/uuid inserted-uuid]))))
(undo-redo/redo test-db)
(is (= recycle-title
(:block/title (:block/page (d/entity @conn [:block/uuid inserted-uuid]))))))))
(deftest undo-works-with-remote-updates-test
(testing "undo works after remote updates on sync graphs"
(undo-redo/clear-history! test-db)
(let [conn (db/get-db test-db false)
{:keys [child-uuid]} (seed-page-parent-child!)]
(d/transact! conn
[[:db/add [:block/uuid child-uuid] :block/title "local-2"]]
{:outliner-op :save-block
:local-tx? true})
(d/transact! conn
[[:db/add [:block/uuid child-uuid] :block/updated-at 12345]]
{:outliner-op :save-block
:local-tx? false})
(let [undo-result (undo-redo/undo test-db)]
(is (not= :frontend.undo-redo/empty-undo-stack undo-result))
(is (= "child" (:block/title (d/entity @conn [:block/uuid child-uuid]))))))))
(deftest undo-redo-works-for-recycle-delete-test
(testing "undo restores a recycled delete and redo sends it back to recycle"
(undo-redo/clear-history! test-db)
(let [conn (db/get-db test-db false)
{:keys [child-uuid page-uuid]} (seed-page-parent-child!)
recycle-page-title "Recycle"]
(outliner-core/delete-blocks! conn [(d/entity @conn [:block/uuid child-uuid])] {})
(let [deleted-child (d/entity @conn [:block/uuid child-uuid])]
(is (integer? (:logseq.property/deleted-at deleted-child)))
(is (= recycle-page-title (:block/title (:block/page deleted-child)))))
(let [undo-result (undo-redo/undo test-db)
restored-child (d/entity @conn [:block/uuid child-uuid])]
(is (not= :frontend.undo-redo/empty-undo-stack undo-result))
(is (= page-uuid (:block/uuid (:block/page restored-child))))
(is (nil? (:logseq.property/deleted-at restored-child))))
(let [redo-result (undo-redo/redo test-db)
recycled-child (d/entity @conn [:block/uuid child-uuid])]
(is (not= :frontend.undo-redo/empty-redo-stack redo-result))
(is (= recycle-page-title (:block/title (:block/page recycled-child))))
(is (integer? (:logseq.property/deleted-at recycled-child)))))))
(deftest undo-validation-allows-baseline-issues-test
(testing "undo validation allows existing issues without introducing new ones"
(let [conn (db/get-db test-db false)
{:keys [child-uuid]} (seed-page-parent-child!)
orphan-uuid (random-uuid)]
(d/transact! conn
[{:block/uuid orphan-uuid
:block/title "orphan"}]
{:local-tx? false})
(is (undo-validate/valid-undo-redo-tx? conn
[[:db/add [:block/uuid child-uuid]
:block/title "child-updated"]])))))
(deftest undo-validation-rejects-invalid-recycle-restore-tx-test
(testing "recycle-shaped undo tx still validates resulting structure"
(let [conn (db/get-db test-db false)
page-uuid (random-uuid)
block-uuid (random-uuid)]
(d/transact! conn
[{:db/ident :logseq.class/Page}
{:block/uuid page-uuid
:block/name "page"
:block/title "page"
:block/tags #{:logseq.class/Page}}
{:db/id 1000
:block/uuid block-uuid
:block/title "bad-block"
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid page-uuid]
:logseq.property/deleted-at 1
:logseq.property.recycle/original-page [:block/uuid page-uuid]
:logseq.property.recycle/original-parent [:block/uuid page-uuid]
:logseq.property.recycle/original-order "aj"}]
{:local-tx? false})
;; Simulate a broken recycled block shell like the runtime repro: entity has
;; structural attrs but no title/uuid dispatch attrs after sync churn.
(d/transact! conn
[[:db/retract 1000 :block/uuid block-uuid]
[:db/retract 1000 :block/title "bad-block"]]
{:local-tx? false})
(is (false? (undo-validate/valid-undo-redo-tx?
conn
[[:db/retract 1000 :logseq.property.recycle/original-order "aj"]
[:db/retract 1000 :logseq.property/deleted-at 1]
[:db/add 1000 :block/parent [:block/uuid page-uuid]]
[:db/retract 1000 :logseq.property.recycle/original-page [:block/uuid page-uuid]]
[:db/retract 1000 :logseq.property.recycle/original-parent [:block/uuid page-uuid]]
[:db/add 1000 :block/order "aj"]
[:db/add 1000 :block/page [:block/uuid page-uuid]]]))))))
(deftest undo-skips-when-parent-missing-test
(testing "undo skips when parent is missing"
(undo-redo/clear-history! test-db)
(let [conn (db/get-db test-db false)
{:keys [parent-uuid child-uuid]} (seed-page-parent-child!)]
(d/transact! conn
[[:db/retractEntity [:block/uuid child-uuid]]]
{:outliner-op :delete-blocks
:local-tx? true})
(d/transact! conn
[[:db/retractEntity [:block/uuid parent-uuid]]]
{:outliner-op :delete-blocks
:local-tx? false})
(is (= :frontend.undo-redo/empty-undo-stack (undo-redo/undo test-db)))
(is (nil? (d/entity @conn [:block/uuid child-uuid]))))))
(deftest undo-skips-when-block-deleted-remote-test
(testing "undo skips when block was deleted remotely"
(undo-redo/clear-history! test-db)
(let [conn (db/get-db test-db false)
{:keys [child-uuid]} (seed-page-parent-child!)]
(d/transact! conn
[[:db/add [:block/uuid child-uuid] :block/title "child-updated"]]
{:outliner-op :save-block
:local-tx? true})
(d/transact! conn
[[:db/retractEntity [:block/uuid child-uuid]]]
{:outliner-op :delete-blocks
:local-tx? false})
(is (= :frontend.undo-redo/empty-undo-stack (undo-redo/undo test-db)))
(is (nil? (d/entity @conn [:block/uuid child-uuid]))))))
(deftest undo-skips-when-undo-would-create-cycle-test
(testing "undo skips when it would create a parent cycle"
(undo-redo/clear-history! test-db)
(let [conn (db/get-db test-db false)
{:keys [page-uuid parent-uuid child-uuid]} (seed-page-parent-child!)]
(d/transact! conn
[[:db/add [:block/uuid child-uuid] :block/parent [:block/uuid page-uuid]]]
{:outliner-op :move-blocks
:local-tx? true})
(d/transact! conn
[[:db/add [:block/uuid parent-uuid] :block/parent [:block/uuid child-uuid]]]
{:outliner-op :move-blocks
:local-tx? false})
(is (= :frontend.undo-redo/empty-undo-stack (undo-redo/undo test-db)))
(let [parent (d/entity @conn [:block/uuid parent-uuid])
child (d/entity @conn [:block/uuid child-uuid])]
(is (= child-uuid (:block/uuid (:block/parent parent))))
(is (= page-uuid (:block/uuid (:block/parent child))))))))
(deftest undo-skips-conflicted-move-and-keeps-earlier-history-test
(testing "undo skips a conflicting move and continues to earlier safe history"
(undo-redo/clear-history! test-db)
(let [conn (db/get-db test-db false)
{:keys [parent-a-uuid parent-b-uuid child-uuid]} (seed-page-two-parents-child!)]
(d/transact! conn
[[:db/add [:block/uuid child-uuid] :block/title "local-title"]]
{:outliner-op :save-block
:local-tx? true})
(d/transact! conn
[[:db/add [:block/uuid child-uuid] :block/parent [:block/uuid parent-b-uuid]]]
{:outliner-op :move-blocks
:local-tx? true})
(d/transact! conn
(:tx-data (outliner-core/delete-blocks @conn [(d/entity @conn [:block/uuid parent-a-uuid])] {}))
{:outliner-op :delete-blocks
:local-tx? false})
(let [undo-result (undo-redo/undo test-db)
child (d/entity @conn [:block/uuid child-uuid])]
(is (map? undo-result))
(is (= "child" (:block/title child)))
(is (= parent-b-uuid
(:block/uuid (:block/parent child))))
(is (empty? (db-issues @conn)))))))
(deftest undo-validation-fast-path-skips-db-issues-for-non-structural-tx-test
(testing "undo validation skips db-issues for non-structural tx-data"
(let [conn (db/get-db test-db false)
{:keys [child-uuid]} (seed-page-parent-child!)]
(with-redefs [undo-validate/issues-for-entity-ids (fn [_ _]
(throw (js/Error. "issues-for-entity-ids called")))]
(is (true? (undo-validate/valid-undo-redo-tx?
conn
[[:db/add [:block/uuid child-uuid] :block/title "child-updated"]])))))))
(deftest undo-validation-checks-structural-tx-test
(testing "undo validation evaluates structural changes"
(let [conn (db/get-db test-db false)
{:keys [page-uuid child-uuid]} (seed-page-parent-child!)
calls (atom 0)]
(with-redefs [undo-validate/issues-for-entity-ids (fn [_ _]
(swap! calls inc)
[])]
(is (true? (undo-validate/valid-undo-redo-tx?
conn
[[:db/add [:block/uuid child-uuid] :block/parent [:block/uuid page-uuid]]])))
(is (pos? @calls))))))
(deftest redo-builds-reversed-tx-when-target-parent-is-recycled-test
(testing "redo still builds reversed tx from raw datoms when target parent was recycled remotely"
(undo-redo/clear-history! test-db)
(let [conn (db/get-db test-db false)
{:keys [child-uuid parent-a-uuid parent-b-uuid]} (seed-page-two-parents-child!)]
(d/transact! conn
[[:db/add [:block/uuid child-uuid] :block/parent [:block/uuid parent-b-uuid]]]
{:outliner-op :move-blocks
:local-tx? true})
(undo-redo/undo test-db)
(d/transact! conn
(:tx-data (outliner-core/delete-blocks @conn [(d/entity @conn [:block/uuid parent-b-uuid])] {}))
{:outliner-op :delete-blocks
:local-tx? false})
(let [redo-op (last (get @undo-redo/*redo-ops test-db))
data (some #(when (= ::undo-redo/db-transact (first %))
(second %))
redo-op)
reversed (undo-redo/get-reversed-datoms conn false data (:tx-meta data))]
(is (seq reversed))
(is (= parent-a-uuid
(:block/uuid (:block/parent (d/entity @conn [:block/uuid child-uuid])))))))))
(deftest undo-skips-move-when-original-parent-is-recycled-test
(testing "undo should skip a move whose original parent has been recycled"
(undo-redo/clear-history! test-db)
(let [conn (db/get-db test-db false)
{:keys [child-uuid parent-a-uuid parent-b-uuid]} (seed-page-two-parents-child!)]
(d/transact! conn
[[:db/add [:block/uuid child-uuid] :block/parent [:block/uuid parent-b-uuid]]]
{:outliner-op :move-blocks
:local-tx? true})
(d/transact! conn
(:tx-data (outliner-core/delete-blocks @conn [(d/entity @conn [:block/uuid parent-a-uuid])] {}))
{:outliner-op :delete-blocks
:local-tx? false})
(let [parent-a (d/entity @conn [:block/uuid parent-a-uuid])
_ (is (some? parent-a))
_ (is (true? (ldb/recycled? parent-a)))
undo-op (last (get @undo-redo/*undo-ops test-db))
data (some #(when (= ::undo-redo/db-transact (first %))
(second %))
undo-op)
conflicted? (#'undo-redo/reversed-structural-target-conflicted?
conn
(->> (:tx-data data) reverse (group-by :e))
true)
reversed (undo-redo/get-reversed-datoms conn true data (:tx-meta data))]
(is (true? conflicted?))
(is (nil? reversed))))))
(deftest ^:long undo-redo-test
(testing "Random mixed operations"
(set! undo-redo/max-stack-length 500)
(let [*random-blocks (atom (outliner-test/get-blocks-ids))]
(outliner-test/transact-random-tree!)
(let [conn (db/get-db test-db false)]
(d/transact! conn
[{:db/ident :logseq.class/Page}
[:db/add [:block/uuid 1] :block/tags :logseq.class/Page]]
{:local-tx? false}))
(let [conn (db/get-db false)
_ (outliner-test/run-random-mixed-ops! *random-blocks)]
(undo-all!)
(is (empty? (db-issues @conn)))
(redo-all!)
(is (empty? (db-issues @conn)))))))
(deftest node-test-undo-redo-does-not-call-worker-test
(let [calls (atom [])
invoke! (fn [& args]
(swap! calls conj (vec args))
(vec args))
repo "repo-node"]
(with-redefs [util/node-test? true
state/<invoke-db-worker invoke!]
(is (= :frontend.undo-redo/empty-undo-stack
(undo-redo/undo repo)))
(is (= :frontend.undo-redo/empty-redo-stack
(undo-redo/redo repo)))
(is (nil? (undo-redo/clear-history! repo)))
(is (empty? @calls)))))

View File

@@ -73,9 +73,9 @@
(let [next-time (get-next-time one-month-ago month-unit 1)]
(is (> (in-days next-time) 1)))
(let [next-time (get-next-time one-month-ago month-unit 3)]
(is (= 2 (in-months next-time))))
(is (contains? #{1 2} (in-months next-time))))
(let [next-time (get-next-time one-month-ago month-unit 5)]
(is (= 4 (in-months next-time))))
(is (contains? #{3 4} (in-months next-time))))
;; year
(let [next-time (get-next-time now year-unit 1)]

View File

@@ -0,0 +1,16 @@
(ns frontend.worker.db-listener-test
(:require [cljs.test :refer [deftest is testing]]
[frontend.worker.db-listener :as db-listener]))
(deftest transit-safe-tx-meta-keeps-outliner-ops-test
(testing "worker tx-meta sanitization should preserve semantic outliner ops"
(let [outliner-ops [[:save-block [{:block/uuid (random-uuid)
:block/title "hello"} nil]]]
tx-meta {:outliner-op :save-block
:outliner-ops outliner-ops
:db-sync/inverse-outliner-ops outliner-ops
:error-handler (fn [_] nil)}
safe-tx-meta (#'db-listener/transit-safe-tx-meta tx-meta)]
(is (= outliner-ops (:outliner-ops safe-tx-meta)))
(is (= outliner-ops (:db-sync/inverse-outliner-ops safe-tx-meta)))
(is (nil? (:error-handler safe-tx-meta))))))

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,21 @@
(ns frontend.worker.db-worker-test
(:require [cljs.test :refer [async deftest is]]
[datascript.core :as d]
[frontend.common.thread-api :as thread-api]
[frontend.worker.a-test-env]
[frontend.worker.db-worker :as db-worker]
[frontend.worker.search :as search]
[frontend.worker.shared-service :as shared-service]
[frontend.worker.state :as worker-state]
[frontend.worker.sync :as db-sync]
[frontend.worker.sync.client-op :as client-op]
[frontend.worker.sync.crypt :as sync-crypt]
[frontend.worker.sync.log-and-state :as rtc-log-and-state]
[logseq.db.frontend.schema :as db-schema]
[promesa.core :as p]))
(:require
[cljs.test :refer [async deftest is]]
[datascript.core :as d]
[frontend.common.thread-api :as thread-api]
[frontend.worker.a-test-env]
[frontend.worker.db-worker :as db-worker]
[frontend.worker.db.validate :as worker-db-validate]
[frontend.worker.search :as search]
[frontend.worker.shared-service :as shared-service]
[frontend.worker.state :as worker-state]
[frontend.worker.sync :as db-sync]
[frontend.worker.sync.client-op :as client-op]
[frontend.worker.sync.crypt :as sync-crypt]
[frontend.worker.sync.download :as sync-download]
[frontend.worker.sync.log-and-state :as rtc-log-and-state]
[logseq.db.frontend.schema :as db-schema]
[promesa.core :as p]))
(def ^:private test-repo "test-db-worker-repo")
(def ^:private close-db!-orig db-worker/close-db!)
@@ -73,14 +76,56 @@
(reset! worker-state/*opfs-pools
{test-repo #js {:pauseVfs (fn [] (swap! pause-calls inc))}})
(reset! search/fuzzy-search-indices {test-repo :stale-cache})
(reset! client-op/*repo->pending-local-tx-count {test-repo 9})
(db-worker/close-db! test-repo)
(is (= #{:db :search :client-ops} (set @closed)))
(is (= 1 @pause-calls))
(is (nil? (get @search/fuzzy-search-indices test-repo)))
(is (nil? (get @client-op/*repo->pending-local-tx-count test-repo)))
(is (nil? (get @worker-state/*sqlite-conns test-repo)))))))
(deftest client-ops-cleanup-timer-starts-once-and-clears-on-close-test
(restoring-worker-state
(fn []
(let [scheduled (atom [])
cleared (atom [])
original-set-interval js/setInterval
original-clear-interval js/clearInterval
fake-db #js {:close (fn [] nil)}
timer-id #js {:id "timer-1"}]
(set! js/setInterval
(fn [f interval-ms]
(swap! scheduled conj {:fn f :interval-ms interval-ms})
timer-id))
(set! js/clearInterval
(fn [id]
(swap! cleared conj id)))
(try
(reset! worker-state/*sqlite-conns
{test-repo {:db fake-db
:search fake-db
:client-ops fake-db}})
(reset! worker-state/*datascript-conns {test-repo :datascript})
(reset! worker-state/*client-ops-conns {test-repo :client-ops})
(reset! (deref #'db-worker/*client-ops-cleanup-timers) {})
(#'db-worker/ensure-client-ops-cleanup-timer! test-repo)
(#'db-worker/ensure-client-ops-cleanup-timer! test-repo)
(is (= 1 (count @scheduled)))
(is (= (* 3 60 60 1000) (:interval-ms (first @scheduled))))
(is (= timer-id (get @(deref #'db-worker/*client-ops-cleanup-timers) test-repo)))
(db-worker/close-db! test-repo)
(is (= [timer-id] @cleared))
(is (nil? (get @(deref #'db-worker/*client-ops-cleanup-timers) test-repo)))
(finally
(set! js/setInterval original-set-interval)
(set! js/clearInterval original-clear-interval)))))))
(deftest complete-datoms-import-invalidates-existing-search-db-test
(async done
(restoring-worker-state
@@ -94,7 +139,7 @@
worker-state/get-sqlite-conn (fn [_repo _type] nil)
client-op/update-local-tx (fn [& _] nil)
shared-service/broadcast-to-clients! (fn [& _] nil)]
(#'db-worker/complete-datoms-import! test-repo "graph-1" 42))
(sync-download/complete-datoms-import! test-repo "graph-1" 42))
(p/then (fn [_]
(is true)
(vreset! thread-api/*thread-apis thread-apis-prev)
@@ -114,21 +159,28 @@
(p/resolved {:error error}))))
(defn- with-fake-create-or-open-db
[repo conn f]
(let [thread-apis-prev @thread-api/*thread-apis]
(vreset! thread-api/*thread-apis
(assoc thread-apis-prev
:thread-api/create-or-open-db
(fn [_repo _opts]
(swap! worker-state/*datascript-conns assoc repo conn)
(p/resolved nil))))
(-> (f)
(p/finally (fn []
(vreset! thread-api/*thread-apis thread-apis-prev))))))
(def sample-datoms
[{:e 1 :a :db/ident :v :logseq.class/Page :tx 1 :added true}
{:e 2 :a :block/title :v "hello" :tx 1 :added true}])
([repo conn f]
(with-fake-create-or-open-db repo conn {} f))
([repo conn
{:keys [create-or-open-db-f close-db-f invalidate-search-db-f unlink-db-f]}
f]
(let [thread-apis-prev @thread-api/*thread-apis
create-or-open-db-f (or create-or-open-db-f
(fn [_repo _opts]
(swap! worker-state/*datascript-conns assoc repo conn)
(p/resolved nil)))
close-db-f (or close-db-f (fn [_repo] nil))
invalidate-search-db-f (or invalidate-search-db-f (fn [_repo] (p/resolved nil)))
unlink-db-f (or unlink-db-f (fn [_repo] nil))]
(vreset! thread-api/*thread-apis
(assoc thread-apis-prev
:thread-api/create-or-open-db create-or-open-db-f
:thread-api/db-sync-close-db close-db-f
:thread-api/db-sync-invalidate-search-db invalidate-search-db-f
:thread-api/unsafe-unlink-db unlink-db-f))
(-> (f)
(p/finally (fn []
(vreset! thread-api/*thread-apis thread-apis-prev)))))))
(deftest db-sync-import-prepare-replaces-active-import-state-test
(async done
@@ -153,82 +205,47 @@
(is false (str error))
(done)))))))))))
(deftest db-sync-import-datoms-chunk-rejects-stale-import-id-test
(deftest db-sync-import-prepare-reset-unlinks-db-before-reopen-test
(async done
(restoring-worker-state
(fn []
(let [prepare (@thread-api/*thread-apis :thread-api/db-sync-import-prepare)
datoms-chunk (@thread-api/*thread-apis :thread-api/db-sync-import-datoms-chunk)
conn (d/create-conn db-schema/schema)]
(with-fake-create-or-open-db
test-repo conn
(fn []
(-> (p/with-redefs [db-worker/close-db! (fn [_] nil)
db-worker/<invalidate-search-db! (fn [_] (p/resolved nil))
rtc-log-and-state/rtc-log (fn [& _] nil)]
(p/let [first-import (prepare test-repo true "graph-1" false)
second-import (prepare test-repo true "graph-1" false)
stale-outcome (capture-outcome #(datoms-chunk sample-datoms "graph-1" (:import-id first-import)))]
(is (= :db-sync/stale-import (some-> stale-outcome :error ex-data :type)))
(is (nil? (d/entity @conn 2)))
(-> (datoms-chunk sample-datoms "graph-1" (:import-id second-import))
(p/then (fn [_]
(is (= "hello" (:block/title (d/entity @conn 2))))
(done))))))
(p/catch (fn [error]
(is false (str error))
(done)))))))))))
(deftest db-sync-import-datoms-chunk-imports-plain-datoms-to-active-db-test
(async done
(restoring-worker-state
(fn []
(let [prepare (@thread-api/*thread-apis :thread-api/db-sync-import-prepare)
datoms-chunk (@thread-api/*thread-apis :thread-api/db-sync-import-datoms-chunk)
conn (d/create-conn db-schema/schema)]
(with-fake-create-or-open-db
test-repo conn
(fn []
(-> (p/with-redefs [db-worker/close-db! (fn [_] nil)
db-worker/<invalidate-search-db! (fn [_] (p/resolved nil))
rtc-log-and-state/rtc-log (fn [& _] nil)]
(p/let [{:keys [import-id]} (prepare test-repo true "graph-1" false)
_ (datoms-chunk sample-datoms "graph-1" import-id)]
(is (= :logseq.class/Page (:db/ident (d/entity @conn 1))))
(is (= "hello" (:block/title (d/entity @conn 2))))
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))))))
(deftest db-sync-import-datoms-chunk-imports-encrypted-datoms-to-active-db-test
(async done
(restoring-worker-state
(fn []
(let [prepare (@thread-api/*thread-apis :thread-api/db-sync-import-prepare)
datoms-chunk (@thread-api/*thread-apis :thread-api/db-sync-import-datoms-chunk)
conn (d/create-conn db-schema/schema)
decrypt-calls (atom [])]
(with-fake-create-or-open-db
test-repo conn
(fn []
(-> (p/with-redefs [db-worker/close-db! (fn [_] nil)
db-worker/<invalidate-search-db! (fn [_] (p/resolved nil))
rtc-log-and-state/rtc-log (fn [& _] nil)
sync-crypt/<fetch-graph-aes-key-for-download (fn [_] (p/resolved :aes-key))
sync-crypt/<decrypt-snapshot-datoms-batch (fn [aes-key datoms]
(swap! decrypt-calls conj {:aes-key aes-key
:datoms datoms})
(p/resolved datoms))]
(p/let [{:keys [import-id]} (prepare test-repo true "graph-1" true)
_ (datoms-chunk sample-datoms "graph-1" import-id)]
(is (= 1 (count @decrypt-calls)))
(is (= sample-datoms (:datoms (first @decrypt-calls))))
(is (= "hello" (:block/title (d/entity @conn 2))))
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))))))
(restoring-worker-state
(fn []
(let [prepare (@thread-api/*thread-apis :thread-api/db-sync-import-prepare)
conn (d/create-conn db-schema/schema)
calls (atom [])]
(with-fake-create-or-open-db
test-repo conn
{:close-db-f (fn [repo]
(swap! calls conj [:close repo])
nil)
:unlink-db-f (fn [repo]
(swap! calls conj [:unlink repo])
nil)
:invalidate-search-db-f (fn [repo]
(swap! calls conj [:invalidate-search repo])
(p/resolved nil))
:create-or-open-db-f (fn [repo opts]
(swap! calls conj [:create-or-open repo opts])
(swap! worker-state/*datascript-conns assoc repo conn)
(p/resolved nil))}
(fn []
(-> (prepare test-repo true "graph-1" false)
(p/then (fn [_]
(let [ops (mapv first @calls)
idx (fn [op]
(first (keep-indexed (fn [i v]
(when (= op v) i))
ops)))]
(is (some? (idx :close)))
(is (some? (idx :unlink)))
(is (some? (idx :invalidate-search)))
(is (some? (idx :create-or-open)))
(is (< (idx :close) (idx :unlink)))
(is (< (idx :unlink) (idx :invalidate-search)))
(is (< (idx :invalidate-search) (idx :create-or-open))))
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))))))
(deftest db-sync-import-finalize-rejects-stale-import-id-test
(async done
@@ -258,33 +275,174 @@
(is false (str error))
(done)))))))))))
(deftest db-sync-import-finalize-completes-active-db-import-test
(deftest db-sync-import-rows-chunk-calls-import-rows-batch-test
(async done
(restoring-worker-state
(fn []
(let [prepare (@thread-api/*thread-apis :thread-api/db-sync-import-prepare)
datoms-chunk (@thread-api/*thread-apis :thread-api/db-sync-import-datoms-chunk)
finalize (@thread-api/*thread-apis :thread-api/db-sync-import-finalize)
rows-chunk (@thread-api/*thread-apis :thread-api/db-sync-import-rows-chunk)
conn (d/create-conn db-schema/schema)
search-db #js {:close (fn [] nil)
:exec (fn [_sql] nil)}
main-db #js {:exec (fn [_sql] nil)}]
(reset! worker-state/*sqlite-conns {test-repo {:db main-db :search search-db :client-ops nil}})
rows [[1 "row-1" nil]
[2 "row-2" nil]]
captured-rows (atom nil)]
(with-fake-create-or-open-db
test-repo conn
(fn []
(-> (p/with-redefs [db-worker/close-db! (fn [_] nil)
db-worker/<invalidate-search-db! (fn [_] (p/resolved nil))
db-sync/rehydrate-large-titles-from-db! (fn [& _] (p/resolved nil))
rtc-log-and-state/rtc-log (fn [& _] nil)
client-op/update-local-tx (fn [& _] nil)
shared-service/broadcast-to-clients! (fn [& _] nil)]
sync-download/<ensure-import-rows-db! (fn [state]
(p/resolved state))
sync-download/import-rows-batch! (fn [_state rows*]
(reset! captured-rows rows*)
(p/resolved 2))]
(p/let [{:keys [import-id]} (prepare test-repo true "graph-1" false)
_ (datoms-chunk sample-datoms "graph-1" import-id)
_ (finalize test-repo "graph-1" 42 import-id)]
(is (= :logseq.class/Page (:db/ident (d/entity @conn 1))))
(is (= "hello" (:block/title (d/entity @conn 2))))
_ (rows-chunk rows "graph-1" import-id)]
(is (= rows @captured-rows))
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))))))
(deftest snapshot-datoms-in-import-order-puts-schema-before-data-test
(let [conn (d/create-conn db-schema/schema)]
(d/transact! conn [{:db/ident :logseq.kv/schema-version
:kv/value {:major 65 :minor 0}}
{:db/ident :user.test/attr
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one}
{:db/id 100
:user.test/attr "hello"}])
(let [ordered (vec (#'sync-download/snapshot-datoms-in-import-order conn))
data-idx (first (keep-indexed (fn [idx datom]
(when (and (= 100 (:e datom))
(= :user.test/attr (:a datom)))
idx))
ordered))
attr-eid (:db/id (d/entity @conn :user.test/attr))
ident-idx (first (keep-indexed (fn [idx datom]
(when (and (= attr-eid (:e datom))
(= :db/ident (:a datom)))
idx))
ordered))
cardinality-idx (first (keep-indexed (fn [idx datom]
(when (and (= attr-eid (:e datom))
(= :db/cardinality (:a datom)))
idx))
ordered))
schema-version-eid (:db/id (d/entity @conn :logseq.kv/schema-version))
schema-version-idx (first (keep-indexed (fn [idx datom]
(when (and (= schema-version-eid (:e datom))
(= :db/ident (:a datom)))
idx))
ordered))]
(is (number? data-idx))
(is (number? ident-idx))
(is (number? cardinality-idx))
(is (number? schema-version-idx))
(is (< schema-version-idx data-idx))
(is (< ident-idx data-idx))
(is (< cardinality-idx data-idx)))))
(deftest import-datoms-batch-transacts-all-db-schema-before-data-test
(async done
(let [conn (d/create-conn db-schema/schema)
attr-eid 8001
datoms [{:e attr-eid :a :db/ident :v :user.test/indexed}
{:e 100 :a :user.test/indexed :v "hello"}
{:e attr-eid :a :db/valueType :v :db.type/string}
{:e attr-eid :a :db/cardinality :v :db.cardinality/one}
{:e attr-eid :a :db/index :v true}]]
(-> (#'sync-download/import-datoms-batch! conn nil false datoms)
(p/then (fn [_]
(is (= 1 (count (d/datoms @conn :avet :user.test/indexed "hello"))))
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest thread-api-validate-db-passes-sync-diagnostics-test
(restoring-worker-state
(fn []
(let [validate (@thread-api/*thread-apis :thread-api/validate-db)
conn (d/create-conn db-schema/schema)
captured (atom nil)
latest-prev @db-sync/*repo->latest-remote-tx]
(reset! worker-state/*datascript-conns {test-repo conn})
(reset! db-sync/*repo->latest-remote-tx {test-repo 11})
(try
(with-redefs [client-op/get-local-tx (fn [_repo] 7)
client-op/get-local-checksum (fn [_repo] "local-checksum")
worker-db-validate/validate-db (fn [& args]
(reset! captured args)
{:ok true})]
(validate test-repo)
(is (= [test-repo
conn
{:local-tx 7
:remote-tx 11
:local-checksum "local-checksum"
:remote-checksum nil}]
@captured)))
(finally
(reset! db-sync/*repo->latest-remote-tx latest-prev)))))))
(deftest thread-api-recompute-checksum-diagnostics-passes-sync-diagnostics-test
(restoring-worker-state
(fn []
(let [recompute (@thread-api/*thread-apis :thread-api/recompute-checksum-diagnostics)
conn (d/create-conn db-schema/schema)
captured (atom nil)
latest-tx-prev @db-sync/*repo->latest-remote-tx
latest-checksum-prev @db-sync/*repo->latest-remote-checksum
result {:recomputed-checksum "recomputed"
:checksum-attrs [:block/uuid]
:blocks []}]
(reset! worker-state/*datascript-conns {test-repo conn})
(reset! db-sync/*repo->latest-remote-tx {test-repo 22})
(reset! db-sync/*repo->latest-remote-checksum {test-repo "remote-checksum"})
(try
(with-redefs [client-op/get-local-tx (fn [_repo] 10)
client-op/get-local-checksum (fn [_repo] "local-checksum")
worker-db-validate/recompute-checksum-diagnostics (fn [& args]
(reset! captured args)
result)]
(is (= (assoc result :local-checksum "recomputed")
(recompute test-repo)))
(is (= [test-repo
conn
{:local-tx 10
:remote-tx 22
:local-checksum "local-checksum"
:remote-checksum "remote-checksum"}]
@captured)))
(finally
(reset! db-sync/*repo->latest-remote-tx latest-tx-prev)
(reset! db-sync/*repo->latest-remote-checksum latest-checksum-prev)))))))
(deftest thread-api-export-client-ops-db-checkpoints-and-exports-client-ops-file-test
(async done
(restoring-worker-state
(fn []
(let [export-client-ops-db (@thread-api/*thread-apis :thread-api/export-client-ops-db)
sql-calls (atom [])
export-calls (atom [])
expected-data (js/Uint8Array. #js [1 2 3])
expected-buffer (.-buffer expected-data)
fake-pool #js {:exportFile (fn [path]
(swap! export-calls conj path)
expected-buffer)}]
(reset! worker-state/*opfs-pools {test-repo fake-pool})
(with-redefs [worker-state/get-sqlite-conn (fn [_repo which-db]
(when (= :client-ops which-db)
#js {:exec (fn [sql]
(swap! sql-calls conj sql))}))]
(-> (export-client-ops-db test-repo)
(p/then (fn [result]
(is (= ["PRAGMA wal_checkpoint(2)"] @sql-calls))
(is (= ["client-ops/db.sqlite"] @export-calls))
(is (instance? js/Uint8Array result))
(is (= [1 2 3] (vec result)))
(done)))
(p/catch (fn [error]
(is false (str error))
(done))))))))))

View File

@@ -3,6 +3,7 @@
[datascript.core :as d]
[frontend.worker.pipeline :as worker-pipeline]
[logseq.db :as ldb]
[logseq.db.common.order :as db-order]
[logseq.db.test.helper :as db-test]))
(deftest test-built-in-page-updates-that-should-be-reverted
@@ -143,3 +144,46 @@
:block/title "page1-renamed"}])]
(is (= "page1-renamed"
(:block/title (d/entity (:db-after result) (:db/id page1)))))))))
(deftest built-in-tag-must-not-convert-page-child-block-to-class-test
(let [conn (db-test/create-conn-with-blocks
{:pages-and-blocks [{:page {:block/title "page1"}}]})
page1 (ldb/get-page @conn "page1")
now (js/Date.now)
bad-block-uuid (random-uuid)
new-tag-uuid (random-uuid)]
(ldb/register-transact-pipeline-fn! worker-pipeline/transact-pipeline)
(testing "page-child block with built-in #Tag stays a block"
(ldb/transact! conn [{:block/uuid bad-block-uuid
:block/title "charlie"
:block/created-at now
:block/updated-at now
:block/page (:db/id page1)
:block/parent (:db/id page1)
:block/order (db-order/gen-key)
:block/tags [:logseq.class/Tag]}])
(let [block (d/entity @conn [:block/uuid bad-block-uuid])]
(is (some? block))
(is (nil? (:db/ident block)))
(is (nil? (:logseq.property.class/extends block)))
(is (not (ldb/class? block)))
(is (= (:db/id page1) (:db/id (:block/parent block))))
(is (empty? (:block/tags block)))))
(testing "standalone candidate is still converted to a class page"
(ldb/transact! conn [{:block/uuid new-tag-uuid
:block/name "standalone-tag"
:block/title "standalone-tag"
:block/created-at now
:block/updated-at now
:block/tags [:logseq.class/Tag]}])
(let [tag-page (d/entity @conn [:block/uuid new-tag-uuid])]
(is (ldb/class? tag-page))
(is (keyword? (:db/ident tag-page)))
(is (= "user.class" (namespace (:db/ident tag-page))))
(is (= [:logseq.class/Root]
(map :db/ident (:logseq.property.class/extends tag-page))))))
;; return global fn back to previous behavior
(ldb/register-transact-pipeline-fn! identity)))

View File

@@ -19,3 +19,14 @@
:logseq.property.reaction/target target-id}])
affected (worker-react/get-affected-queries-keys tx-report)]
(is (some #{[:frontend.worker.react/block-reactions target-id]} affected)))))
(deftest affected-keys-journals-when-journal-recycled
(testing "recycling a journal page should refresh journals query key"
(let [conn (db-test/create-conn-with-blocks
[{:page {:build/journal 20240101}}
{:page {:build/journal 20240102}}])
journal (db-test/find-journal-by-journal-day @conn 20240102)
tx-report (d/transact! conn [{:db/id (:db/id journal)
:logseq.property/deleted-at 1704196800000}])
affected (worker-react/get-affected-queries-keys tx-report)]
(is (some #{[:frontend.worker.react/journals]} affected)))))

View File

@@ -1,19 +1,122 @@
(ns frontend.worker.sync.client-op-test
(:require [cljs.test :refer [deftest is]]
[datascript.core :as d]
(:require [cljs.test :refer [deftest is testing]]
[frontend.worker.state :as worker-state]
[frontend.worker.sync.client-op :as client-op]))
(deftest update-graph-uuid-replaces-existing-value-test
(let [repo "repo-1"
conn (d/create-conn client-op/schema-in-db)
(defn- new-memory-db
[]
(let [Database (js/require "better-sqlite3")]
(new Database ":memory:")))
(defn- with-client-ops-db
[repo f]
(let [db (new-memory-db)
prev-client-ops-conns @worker-state/*client-ops-conns]
(reset! worker-state/*client-ops-conns {repo conn})
(reset! worker-state/*client-ops-conns {repo db})
(try
(client-op/update-graph-uuid repo "graph-1")
(client-op/update-graph-uuid repo "graph-2")
(let [graph-uuid-datoms (vec (d/datoms @conn :avet :graph-uuid))]
(is (= 1 (count graph-uuid-datoms)))
(is (= #{"graph-2"} (set (map :v graph-uuid-datoms)))))
(f db)
(finally
(.close db)
(reset! worker-state/*client-ops-conns prev-client-ops-conns)))))
(defn- sqlite-count
[^js db sql & args]
(let [^js stmt (.prepare db sql)
^js row (if (seq args)
(.apply (.-get stmt) stmt (to-array args))
(.get stmt))]
(if row
(or (aget row "c")
(aget row "count"))
0)))
(deftest sqlite-sync-meta-roundtrip-test
(let [repo "repo-1"]
(with-client-ops-db
repo
(fn [_db]
(client-op/update-graph-uuid repo "graph-1")
(client-op/update-local-tx repo 9)
(client-op/update-local-checksum repo "checksum-1")
(client-op/update-graph-uuid repo "graph-2")
(client-op/update-local-tx repo 12)
(client-op/update-local-checksum repo "checksum-2")
(is (= "graph-2" (client-op/get-graph-uuid repo)))
(is (= 12 (client-op/get-local-tx repo)))
(is (= "checksum-2" (client-op/get-local-checksum repo)))))))
(deftest sqlite-asset-ops-coalescing-test
(let [repo "repo-asset"
asset-uuid (random-uuid)]
(with-client-ops-db
repo
(fn [_db]
(client-op/add-asset-ops repo [[:update-asset 10 {:block-uuid asset-uuid}]])
(is (= 1 (client-op/get-unpushed-asset-ops-count repo)))
(is (= [:update-asset 10 {:block-uuid asset-uuid}]
(:update-asset (first (client-op/get-all-asset-ops repo)))))
;; older remove should be ignored because a newer update already exists
(client-op/add-asset-ops repo [[:remove-asset 9 {:block-uuid asset-uuid}]])
(is (= [:update-asset 10 {:block-uuid asset-uuid}]
(:update-asset (first (client-op/get-all-asset-ops repo)))))
;; newer remove should replace update
(client-op/add-asset-ops repo [[:remove-asset 11 {:block-uuid asset-uuid}]])
(is (= [:remove-asset 11 {:block-uuid asset-uuid}]
(:remove-asset (first (client-op/get-all-asset-ops repo)))))
(client-op/remove-asset-op repo asset-uuid)
(is (= 0 (client-op/get-unpushed-asset-ops-count repo)))))))
(deftest cleanup-finished-history-ops-removes-only-unreferenced-finished-txs-test
(let [repo "repo-cleanup"
keep-tx-id (random-uuid)
remove-tx-id (random-uuid)
pending-tx-id (random-uuid)]
(with-client-ops-db
repo
(fn [db]
(client-op/update-local-tx repo 99)
(client-op/upsert-local-tx-entry!
repo
{:tx-id keep-tx-id
:created-at 1
:pending? false
:failed? false
:normalized-tx-data []
:reversed-tx-data []})
(client-op/upsert-local-tx-entry!
repo
{:tx-id remove-tx-id
:created-at 2
:pending? false
:failed? false
:normalized-tx-data []
:reversed-tx-data []})
(client-op/upsert-local-tx-entry!
repo
{:tx-id pending-tx-id
:created-at 3
:pending? true
:failed? false
:normalized-tx-data []
:reversed-tx-data []})
(is (= 1 (client-op/cleanup-finished-history-ops! repo #{keep-tx-id})))
(is (= 1 (sqlite-count db "select count(*) as c from client_ops where tx_id = ?" (str keep-tx-id))))
(is (= 0 (sqlite-count db "select count(*) as c from client_ops where tx_id = ?" (str remove-tx-id))))
(is (= 1 (sqlite-count db "select count(*) as c from client_ops where tx_id = ?" (str pending-tx-id))))
(is (= 99 (client-op/get-local-tx repo)))))))
(deftest cleanup-finished-history-ops-no-conn-is-noop-test
(let [repo "repo-no-conn"
prev-client-ops-conns @worker-state/*client-ops-conns]
(reset! worker-state/*client-ops-conns {})
(try
(testing "cleanup should be safe when client-ops conn is missing"
(is (= 0 (client-op/cleanup-finished-history-ops! repo #{}))))
(finally
(reset! worker-state/*client-ops-conns prev-client-ops-conns)))))

View File

@@ -46,3 +46,85 @@
(p/catch (fn [e]
(is false (str e))
(done))))))
(deftest fetch-graph-aes-key-for-download-retries-with-fresh-rsa-key-pair-test
(async done
(let [clear-user-rsa-cache-calls* (atom 0)
get-pair-calls* (atom 0)]
(-> (p/with-redefs [sync-crypt/e2ee-base (fn [] "https://sync.example.test")
sync-crypt/get-user-uuid (fn [] "user-1")
sync-crypt/<clear-item! (fn [_] (p/resolved nil))
sync-crypt/<set-item! (fn [_ _] (p/resolved nil))
sync-crypt/<clear-user-rsa-key-pair-cache! (fn [_base _user-id]
(swap! clear-user-rsa-cache-calls* inc)
(p/resolved nil))
sync-crypt/<get-user-rsa-key-pair-raw (fn [_base]
(swap! get-pair-calls* inc)
(if (= 1 @get-pair-calls*)
(p/resolved {:public-key "pk-old"
:encrypted-private-key "enc-old"})
(p/resolved {:public-key "pk-new"
:encrypted-private-key "enc-new"})))
sync-crypt/<decrypt-private-key (fn [encrypted-private-key]
(p/resolved
(case encrypted-private-key
"enc-old" :private-key-old
"enc-new" :private-key-new
:private-key-unknown)))
sync-crypt/<fetch-graph-encrypted-aes-key-raw (fn [_base _graph-id]
(p/resolved {:encrypted-aes-key
(ldb/write-transit-str "encrypted-aes")}))
crypt/<decrypt-aes-key (fn [private-key encrypted-aes-key]
(if (= :private-key-old private-key)
(p/rejected (ex-info "decrypt-aes-key" {}))
(p/resolved [:aes-key private-key encrypted-aes-key])))]
(sync-crypt/<fetch-graph-aes-key-for-download "graph-1"))
(p/then (fn [result]
(is (= [:aes-key :private-key-new "encrypted-aes"] result))
(is (= 1 @clear-user-rsa-cache-calls*))
(is (= 2 @get-pair-calls*))
(done)))
(p/catch (fn [e]
(is false (str e))
(done)))))))
(deftest fetch-graph-aes-key-for-download-rethrows-without-user-id-test
(async done
(let [clear-user-rsa-cache-calls* (atom 0)]
(-> (p/with-redefs [sync-crypt/e2ee-base (fn [] "https://sync.example.test")
sync-crypt/get-user-uuid (fn [] nil)
sync-crypt/<clear-item! (fn [_] (p/resolved nil))
sync-crypt/<set-item! (fn [_ _] (p/resolved nil))
sync-crypt/<clear-user-rsa-key-pair-cache! (fn [_base _user-id]
(swap! clear-user-rsa-cache-calls* inc)
(p/resolved nil))
sync-crypt/<get-user-rsa-key-pair-raw (fn [_base]
(p/resolved {:public-key "pk-old"
:encrypted-private-key "enc-old"}))
sync-crypt/<decrypt-private-key (fn [_] (p/resolved :private-key-old))
sync-crypt/<fetch-graph-encrypted-aes-key-raw (fn [_base _graph-id]
(p/resolved {:encrypted-aes-key
(ldb/write-transit-str "encrypted-aes")}))
crypt/<decrypt-aes-key (fn [_ _]
(p/rejected (ex-info "decrypt-aes-key" {})))]
(sync-crypt/<fetch-graph-aes-key-for-download "graph-1"))
(p/then (fn [_]
(is false "expected decrypt-aes-key failure")
(done)))
(p/catch (fn [e]
(is (= "decrypt-aes-key" (ex-message e)))
(is (zero? @clear-user-rsa-cache-calls*))
(done)))))))
(deftest decrypt-text-value-legacy-plaintext-test
(async done
(-> (p/let [aes-key (crypt/<generate-aes-key)
plaintext "$$$favorites"
encrypted (crypt/<encrypt-uint8array aes-key (.encode (js/TextEncoder.) plaintext))
encrypted-str (ldb/write-transit-str encrypted)
decrypted (sync-crypt/<decrypt-text-value aes-key encrypted-str)]
(is (= plaintext decrypted))
(done))
(p/catch (fn [e]
(is false (str e))
(done))))))

View File

@@ -0,0 +1,44 @@
(ns frontend.worker.sync.download-test
(:require [cljs.test :refer [async deftest is]]
[frontend.worker.sync.download :as sync-download]
[logseq.db-sync.snapshot :as snapshot]
[promesa.core :as p]))
(defn- frame-bytes
[^js data]
(let [len (.-byteLength data)
out (js/Uint8Array. (+ 4 len))
view (js/DataView. (.-buffer out))]
(.setUint32 view 0 len false)
(.set out data 4)
out))
(defn- stream-from-payload
[^js payload]
(js/ReadableStream.
#js {:start (fn [controller]
(.enqueue controller payload)
(.close controller))}))
(deftest stream-snapshot-row-batches-ignores-stale-gzip-header-test
(async done
(let [rows [[1 "row-1" nil]
[2 "row-2" nil]]
payload (frame-bytes (snapshot/encode-rows rows))
resp (js/Response.
(stream-from-payload payload)
#js {:status 200
:headers #js {"content-encoding" "gzip"}})
batches* (atom [])]
(-> (#'sync-download/<stream-snapshot-row-batches!
resp
1000
(fn [batch]
(swap! batches* conj batch)
(p/resolved true)))
(p/then (fn [_]
(is (= [rows] @batches*))
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))

File diff suppressed because it is too large Load Diff