Initial import of LogDarioSeq baseline

Establishes version control for the personal Logseq DB fork before
starting the server-client architecture migration and domain features
described in docs/agent-guide and the approved implementation plan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 15:32:15 +02:00
commit 40631362b3
2016 changed files with 543898 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
(ns logseq.db-sync.batch-test
(:require [cljs.test :refer [deftest is testing]]
[logseq.db-sync.batch :as batch]))
(deftest rows->insert-batches-test
(testing "splits rows into batches based on max params"
(let [rows [[1 "a" nil]
[2 "b" nil]
[3 "c" nil]]
batches (batch/rows->insert-batches "kvs_import" rows {:max-params 6})]
(is (= 2 (count batches)))
(is (= [1 "a" nil 2 "b" nil] (:args (first batches))))
(is (= [3 "c" nil] (:args (second batches))))
(is (string? (:sql (first batches))))
(is (string? (:sql (second batches))))))
(testing "empty rows returns no batches"
(is (= [] (batch/rows->insert-batches "kvs_import" [] {:max-params 6})))))

View File

@@ -0,0 +1,547 @@
(ns logseq.db-sync.checksum-test
(:require ["fs" :as fs]
[cljs.reader :as reader]
[cljs.test :refer [deftest is testing]]
[datascript.core :as d]
[logseq.db :as ldb]
[logseq.db-sync.checksum :as checksum]
[logseq.db.frontend.schema :as db-schema]))
(defn- sample-db
[]
(let [page-a-uuid (random-uuid)
page-b-uuid (random-uuid)
parent-uuid (random-uuid)
child-uuid (random-uuid)]
(-> (d/empty-db db-schema/schema)
(d/db-with [{:db/id 1
:block/uuid page-a-uuid
:block/name "page-a"
:block/title "Page A"}
{:db/id 2
:block/uuid page-b-uuid
:block/name "page-b"
:block/title "Page B"}
{:db/id 3
:block/uuid parent-uuid
:block/title "Parent"
:block/parent 1
:block/page 1}
{:db/id 4
:block/uuid child-uuid
:block/title "Child"
:block/parent 3
:block/page 1}]))))
(defn- assert-incremental=full!
[db-before checksum-before tx-data]
(let [tx-report (d/with db-before tx-data)
full (checksum/recompute-checksum (:db-after tx-report))
incremental (checksum/update-checksum checksum-before tx-report)]
(is (= full incremental)
(str "Expected checksum parity for tx-data: " (pr-str tx-data)))
{:db (:db-after tx-report)
:checksum incremental}))
(defn- load-rebased-retract-checksum-fixture
[]
(let [payload (-> (.readFileSync fs "test/logseq/db_sync/fixtures/rebased_retract_checksum_payload.edn" "utf8")
reader/read-string)]
{:db-before (ldb/read-transit-str (:db-before payload))
:db-after (ldb/read-transit-str (:db-after payload))
:tx-data (ldb/read-transit-str (:tx-data payload))}))
(defn- load-parent-order-rebase-checksum-fixture
[]
(let [payload (-> (.readFileSync fs "test/logseq/db_sync/fixtures/parent_order_rebase_checksum_payload.edn" "utf8")
reader/read-string)]
{:db-before (ldb/read-transit-str (:db-before payload))
:db-after (ldb/read-transit-str (:db-after payload))
:tx-data (ldb/read-transit-str (:tx-data payload))}))
(deftest checksum-ignores-unrelated-datoms-test
(testing "recompute and incremental checksums ignore unrelated datoms"
(let [db-before (sample-db)
checksum-before (checksum/recompute-checksum db-before)
tx-data [[:db/add 4 :block/updated-at 1773661308002]
[:db/add 4 :logseq.property/created-by-ref 99]]
tx-report (d/with db-before tx-data)]
(is (= checksum-before
(checksum/recompute-checksum (:db-after tx-report))))
(is (= checksum-before
(checksum/update-checksum checksum-before tx-report))))))
(deftest incremental-checksum-matches-recompute-on-rebased-retract-entity-log-repro-test
(testing "incremental checksum should equal full recompute on rebased retract-entity replay payload"
(let [{:keys [db-before db-after tx-data]} (load-rebased-retract-checksum-fixture)
checksum-before (checksum/recompute-checksum db-before)
tx-report {:db-before db-before
:db-after db-after
:tx-data tx-data}
full (checksum/recompute-checksum db-after)
incremental (checksum/update-checksum checksum-before tx-report)]
(is (not= checksum-before full))
(is (= full incremental)))))
(deftest incremental-checksum-matches-recompute-on-parent-order-rebase-log-repro-test
(testing "incremental checksum should equal full recompute on parent/order rebase payload"
(let [{:keys [db-before db-after tx-data]} (load-parent-order-rebase-checksum-fixture)
checksum-before (checksum/recompute-checksum db-before)
tx-report {:db-before db-before
:db-after db-after
:tx-data tx-data}
full (checksum/recompute-checksum db-after)
incremental (checksum/update-checksum checksum-before tx-report)]
(is (= full incremental)))))
(deftest incremental-checksum-matches-recompute-on-replace-datom-test
(testing "incremental checksum matches full recompute when replacing existing values"
(let [db-before (sample-db)
tx-report (d/with db-before [[:db/add 4 :block/title "Child updated"]
[:db/add 1 :block/name "page-a-updated"]])]
(is (= (checksum/recompute-checksum (:db-after tx-report))
(checksum/update-checksum (checksum/recompute-checksum db-before) tx-report))))))
(deftest incremental-checksum-matches-recompute-across-mixed-mutations-test
(testing "incremental checksum stays equal to full recompute across typical tx sequences"
(let [db0 (sample-db)
new-block-uuid (random-uuid)
{:keys [db checksum]} (reduce
(fn [{:keys [db checksum]} {:keys [tx-data]}]
(assert-incremental=full! db checksum tx-data))
{:db db0
:checksum (checksum/recompute-checksum db0)}
[{:tx-data [[:db/add 4 :block/title "Child edited"]]}
{:tx-data [[:db/add 1 :block/name "page-a-renamed"]
[:db/add 1 :block/title "Page A Renamed"]]}
{:tx-data [[:db/add 4 :block/parent 2]
[:db/add 4 :block/page 2]]}
{:tx-data [[:db/add -1 :block/uuid new-block-uuid]
[:db/add -1 :block/title "New block"]
[:db/add -1 :block/parent 2]
[:db/add -1 :block/page 2]]}
{:tx-data [[:db/retract 3 :block/title "Parent"]]}
{:tx-data [[:db/retractEntity [:block/uuid new-block-uuid]]]}
{:tx-data [[:db/add 4 :block/updated-at 1773661308002]]}])]
(is (= checksum (checksum/recompute-checksum db))))))
(deftest incremental-checksum-uses-recompute-when-initial-checksum-missing-test
(testing "nil initial checksum uses db-before recompute as baseline"
(let [db-before (sample-db)
tx-report (d/with db-before [[:db/add 4 :block/title "Child updated"]])]
(is (= (checksum/recompute-checksum (:db-after tx-report))
(checksum/update-checksum nil tx-report))))))
(deftest checksum-e2ee-ignores-title-and-name-test
(testing "with E2EE enabled, checksum ignores title/name changes for both modes"
(let [db-before (-> (sample-db)
(d/db-with [{:db/ident :logseq.kv/graph-rtc-e2ee?
:kv/value true}]))
checksum-before (checksum/recompute-checksum db-before)
tx-report (d/with db-before [[:db/add 4 :block/title "Encrypted title update"]
[:db/add 1 :block/name "encrypted-name-update"]])]
(is (= checksum-before
(checksum/recompute-checksum (:db-after tx-report))))
(is (= checksum-before
(checksum/update-checksum checksum-before tx-report))))))
(deftest incremental-checksum-recomputes-when-e2ee-mode-toggles-test
(testing "incremental checksum falls back to full recompute when E2EE mode changes"
(let [db-before (sample-db)
tx-report (d/with db-before [{:db/ident :logseq.kv/graph-rtc-e2ee?
:kv/value true}])]
(is (= (checksum/recompute-checksum (:db-after tx-report))
(checksum/update-checksum (checksum/recompute-checksum db-before) tx-report))))))
(deftest incremental-checksum-matches-recompute-when-referenced-entity-disappears-test
(testing "incremental checksum tracks blocks whose parent/page UUID becomes unresolved after retracting referenced entities"
(let [db-before (sample-db)
before-checksum (checksum/recompute-checksum db-before)
tx-report (d/with db-before [[:db/retractEntity 3]
[:db/retractEntity 1]])
db-after (:db-after tx-report)
full (checksum/recompute-checksum db-after)
incremental (checksum/update-checksum before-checksum tx-report)]
(is (not= before-checksum full))
(is (= full incremental)))))
(deftest incremental-checksum-matches-recompute-when-parent-uuid-changes-test
(testing "incremental checksum tracks children whose normalized parent UUID changes via parent :block/uuid update"
(let [db-before (sample-db)
before-checksum (checksum/recompute-checksum db-before)
tx-report (d/with db-before [[:db/add 3 :block/uuid (random-uuid)]])
db-after (:db-after tx-report)
full (checksum/recompute-checksum db-after)
incremental (checksum/update-checksum before-checksum tx-report)]
(is (not= before-checksum full))
(is (= full incremental)))))
(deftest incremental-checksum-matches-recompute-when-page-uuid-changes-test
(testing "incremental checksum tracks blocks whose normalized page UUID changes via page :block/uuid update"
(let [db-before (sample-db)
before-checksum (checksum/recompute-checksum db-before)
tx-report (d/with db-before [[:db/add 1 :block/uuid (random-uuid)]])
db-after (:db-after tx-report)
full (checksum/recompute-checksum db-after)
incremental (checksum/update-checksum before-checksum tx-report)]
(is (not= before-checksum full))
(is (= full incremental)))))
(deftest incremental-checksum-matches-recompute-when-parent-uuid-changes-on-ineligible-target-test
(testing "incremental checksum tracks children when parent UUID changes on a non-checksum-eligible target entity"
(let [db-before (-> (d/empty-db db-schema/schema)
(d/db-with [{:db/id 1
:block/uuid (random-uuid)
:block/name "page-a"
:block/title "Page A"}
{:db/id 2
:block/uuid (random-uuid)
:block/title "Orphan parent"}
{:db/id 3
:block/uuid (random-uuid)
:block/title "Child"
:block/parent 2
:block/page 1}]))
before-checksum (checksum/recompute-checksum db-before)
tx-report (d/with db-before [[:db/add 2 :block/uuid (random-uuid)]])
db-after (:db-after tx-report)
full (checksum/recompute-checksum db-after)
incremental (checksum/update-checksum before-checksum tx-report)]
(is (not= before-checksum full))
(is (= full incremental)))))
(deftest incremental-checksum-matches-recompute-when-ineligible-referenced-entity-is-retracted-test
(testing "incremental checksum tracks children when normalized tx data retracts a referenced non-checksum-eligible entity"
(let [parent-uuid (random-uuid)
child-uuid (random-uuid)
page-uuid (random-uuid)
db-before (-> (d/empty-db db-schema/schema)
(d/db-with [{:db/id 1
:block/uuid page-uuid
:block/name "page-a"
:block/title "Page A"}
{:db/id 2
:block/uuid parent-uuid
:block/title "Orphan parent"}
{:db/id 3
:block/uuid child-uuid
:block/title "Child"
:block/parent 2
:block/page 1}]))
before-checksum (checksum/recompute-checksum db-before)
tx-report* (d/with db-before [[:db/retractEntity [:block/uuid parent-uuid]]])
tx-report (assoc tx-report*
:tx-data [[:db/retractEntity [:block/uuid parent-uuid]]])
full (checksum/recompute-checksum (:db-after tx-report))
incremental (checksum/update-checksum before-checksum tx-report)]
(is (not= before-checksum full))
(is (= full incremental)))))
(deftest incremental-checksum-matches-recompute-when-block-is-readded-test
(testing "incremental checksum remains equal to recompute when a block is deleted and re-added with the same UUID"
(let [db0 (sample-db)
checksum0 (checksum/recompute-checksum db0)
child-uuid (:block/uuid (d/entity db0 4))
parent-uuid (:block/uuid (d/entity db0 3))
page-uuid (:block/uuid (d/entity db0 1))
{:keys [db checksum]} (assert-incremental=full! db0 checksum0 [[:db/retractEntity [:block/uuid child-uuid]]])]
(assert-incremental=full! db checksum [{:db/id -1
:block/uuid child-uuid
:block/title "Child"
:block/parent [:block/uuid parent-uuid]
:block/page [:block/uuid page-uuid]}]))))
(deftest incremental-checksum-matches-recompute-when-block-is-readded-with-uuid-tempid-test
(testing "server rebase txs can delete and recreate the same block using a UUID string tempid"
(let [db0 (sample-db)
checksum0 (checksum/recompute-checksum db0)
block-uuid (random-uuid)
block-tempid (str block-uuid)
page-uuid (:block/uuid (d/entity db0 1))
parent-uuid (:block/uuid (d/entity db0 3))
tx-data [[:db/add block-tempid :block/uuid block-uuid 536871536]
[:db/add block-tempid :block/title "undo redo block" 536871536]
[:db/add block-tempid :block/parent [:block/uuid parent-uuid] 536871536]
[:db/add block-tempid :block/order "a0" 536871536]
[:db/add block-tempid :block/page [:block/uuid page-uuid] 536871536]]
{:keys [db checksum]} (assert-incremental=full! db0 checksum0 tx-data)
{:keys [db checksum]} (assert-incremental=full! db checksum [[:db/retractEntity [:block/uuid block-uuid]]])]
(assert-incremental=full!
db
checksum
[[:db/add block-tempid :block/uuid block-uuid 536871538]
[:db/add block-tempid :block/title "undo redo block" 536871538]
[:db/add block-tempid :block/parent [:block/uuid parent-uuid] 536871538]
[:db/add block-tempid :block/order "a0" 536871538]
[:db/add block-tempid :block/page [:block/uuid page-uuid] 536871538]]))))
(deftest incremental-checksum-matches-recompute-with-normalized-vector-tx-data-test
(testing "incremental checksum tracks normalized tx vectors persisted in server tx_log"
(let [db-before (sample-db)
checksum-before (checksum/recompute-checksum db-before)
block-uuid (random-uuid)
block-tempid (str block-uuid)
parent-uuid (:block/uuid (d/entity db-before 3))
page-uuid (:block/uuid (d/entity db-before 1))
normalized-tx-data [[:db/add block-tempid :block/uuid block-uuid 536871538]
[:db/add block-tempid :block/title "normalized block" 536871538]
[:db/add block-tempid :block/parent [:block/uuid parent-uuid] 536871538]
[:db/add block-tempid :block/order "a0" 536871538]
[:db/add block-tempid :block/page [:block/uuid page-uuid] 536871538]]
tx-report (d/with db-before normalized-tx-data)
full (checksum/recompute-checksum (:db-after tx-report))
incremental (checksum/update-checksum
checksum-before
(assoc tx-report :tx-data normalized-tx-data))]
(is (not= checksum-before full))
(is (= full incremental)))))
(deftest incremental-checksum-matches-recompute-when-delete-tree-undo-and-delete-again-test
(testing "incremental checksum matches recompute across delete-tree, undo-all, then delete-tree-again"
(let [db0 (sample-db)
parent-uuid (:block/uuid (d/entity db0 3))
child-uuid (:block/uuid (d/entity db0 4))
page-uuid (:block/uuid (d/entity db0 1))
tx-seq [{:tx-data [[:db/retractEntity [:block/uuid child-uuid]]
[:db/retractEntity [:block/uuid parent-uuid]]]}
{:tx-data [{:db/id -1
:block/uuid parent-uuid
:block/title "Parent"
:block/parent [:block/uuid page-uuid]
:block/page [:block/uuid page-uuid]}
{:db/id -2
:block/uuid child-uuid
:block/title "Child"
:block/parent [:block/uuid parent-uuid]
:block/page [:block/uuid page-uuid]}]}
{:tx-data [[:db/retractEntity [:block/uuid child-uuid]]
[:db/retractEntity [:block/uuid parent-uuid]]]}]
{:keys [db checksum]} (reduce
(fn [{:keys [db checksum]} {:keys [tx-data]}]
(assert-incremental=full! db checksum tx-data))
{:db db0
:checksum (checksum/recompute-checksum db0)}
tx-seq)]
(is (= checksum (checksum/recompute-checksum db))))))
(deftest recompute-checksum-diagnostics-includes-relevant-attrs-test
(testing "diagnostics includes checksum attrs and block values used for checksum export"
(let [db (sample-db)
{:keys [checksum attrs blocks e2ee?]} (checksum/recompute-checksum-diagnostics db)
child-uuid (:block/uuid (d/entity db 4))
child-parent-uuid (:block/uuid (:block/parent (d/entity db 4)))
child-page-uuid (:block/uuid (:block/page (d/entity db 4)))
child (some #(when (= child-uuid (:block/uuid %)) %) blocks)]
(is (false? e2ee?))
(is (= (checksum/recompute-checksum db) checksum))
(is (= #{:block/uuid :block/title :block/name :block/parent :block/page :block/order}
(set attrs)))
(is (= 4 (count blocks)))
(is (= child-parent-uuid (:block/parent child)))
(is (= child-page-uuid (:block/page child)))
(is (string? (:block/title child))))))
(deftest recompute-checksum-diagnostics-omits-title-and-name-in-e2ee-test
(testing "diagnostics for E2EE graphs omits title/name from checksum attrs and export blocks"
(let [db (-> (sample-db)
(d/db-with [{:db/ident :logseq.kv/graph-rtc-e2ee?
:kv/value true}]))
{:keys [checksum attrs blocks e2ee?]} (checksum/recompute-checksum-diagnostics db)]
(is e2ee?)
(is (= (checksum/recompute-checksum db) checksum))
(is (= #{:block/uuid :block/parent :block/page :block/order}
(set attrs)))
(is (every? #(not (contains? % :block/title)) blocks))
(is (every? #(not (contains? % :block/name)) blocks)))))
(deftest incremental-checksum-is-invariant-across-tx-partitioning-test
(testing "incremental checksum converges to the same value regardless of tx partitioning"
(let [db0 (sample-db)
tx-a [[:db/add 4 :block/order "aBL"]
[:db/add 4 :block/title "Child v2"]]
tx-b [[:db/add 3 :block/order "aBK"]
[:db/add 4 :block/parent 2]
[:db/add 4 :block/page 2]]
one-shot-report (d/with db0 (into tx-a tx-b))
one-shot-checksum (checksum/update-checksum (checksum/recompute-checksum db0)
one-shot-report)
checksum0 (checksum/recompute-checksum db0)
report-a (d/with db0 tx-a)
checksum-a (checksum/update-checksum checksum0 report-a)
db-a (:db-after report-a)
report-b (d/with db-a tx-b)
checksum-b (checksum/update-checksum checksum-a report-b)
db-final (:db-after report-b)
full-final (checksum/recompute-checksum db-final)]
(is (= full-final one-shot-checksum))
(is (= full-final checksum-b))
(is (= one-shot-checksum checksum-b)))))
(deftest incremental-checksum-is-invariant-across-commuting-batch-order-test
(testing "incremental checksum converges when commuting tx batches are applied in different order"
(let [db0 (sample-db)
checksum0 (checksum/recompute-checksum db0)
tx-a [[:db/add 3 :block/title "Parent v2"]
[:db/add 4 :block/order "a9"]]
tx-b [[:db/add 1 :block/name "page-a-v2"]
[:db/add 2 :block/title "Page B v2"]]
report-a (d/with db0 tx-a)
checksum-a (checksum/update-checksum checksum0 report-a)
db-a (:db-after report-a)
report-b-after-a (d/with db-a tx-b)
checksum-ab (checksum/update-checksum checksum-a report-b-after-a)
db-ab (:db-after report-b-after-a)
full-ab (checksum/recompute-checksum db-ab)
report-b (d/with db0 tx-b)
checksum-b (checksum/update-checksum checksum0 report-b)
db-b (:db-after report-b)
report-a-after-b (d/with db-b tx-a)
checksum-ba (checksum/update-checksum checksum-b report-a-after-b)
db-ba (:db-after report-a-after-b)
full-ba (checksum/recompute-checksum db-ba)]
(is (= full-ab full-ba))
(is (= full-ab checksum-ab))
(is (= full-ba checksum-ba))
(is (= checksum-ab checksum-ba)))))
(deftest incremental-checksum-is-invariant-across-intra-batch-datom-order-test
(testing "incremental checksum converges when same tx-data uses different datom order"
(let [db0 (sample-db)
checksum0 (checksum/recompute-checksum db0)
uuid-a (random-uuid)
uuid-b (random-uuid)
tx-order-a [{:db/id -1
:block/uuid uuid-a
:block/title "Inserted A"
:block/order "a5"
:block/parent 3
:block/page 1}
{:db/id -2
:block/uuid uuid-b
:block/title "Inserted B"
:block/order "a6"
:block/parent 3
:block/page 1}]
tx-order-b (vec (reverse tx-order-a))
report-a (d/with db0 tx-order-a)
db-a (:db-after report-a)
full-a (checksum/recompute-checksum db-a)
incremental-a (checksum/update-checksum checksum0 report-a)
report-b (d/with db0 tx-order-b)
db-b (:db-after report-b)
full-b (checksum/recompute-checksum db-b)
incremental-b (checksum/update-checksum checksum0 report-b)]
(is (= full-a full-b))
(is (= full-a incremental-a))
(is (= full-b incremental-b))
(is (= incremental-a incremental-b)))))
(deftest incremental-checksum-handles-rebase-like-toggle-churn-test
(testing "incremental checksum uses net tuple delta when batch contains add/retract/add churn"
(let [db0 (sample-db)
checksum0 (checksum/recompute-checksum db0)
;; Simulate rebase churn on a tuple absent in db0: add -> retract -> add
report-1 (d/with db0 [[:db/add 4 :block/order "aBL"]])
db1 (:db-after report-1)
report-2 (d/with db1 [[:db/retract 4 :block/order "aBL"]])
db2 (:db-after report-2)
report-3 (d/with db2 [[:db/add 4 :block/order "aBL"]])
db3 (:db-after report-3)
batch-report {:db-before db0
:db-after db3
:tx-data (vec (concat (:tx-data report-1)
(:tx-data report-2)
(:tx-data report-3)))}
full-final (checksum/recompute-checksum db3)
incremental (checksum/update-checksum checksum0 batch-report)]
(is (not= checksum0 full-final))
(is (= full-final incremental)))))
(deftest incremental-checksum-handles-transient-entity-churn-in-one-batch-test
(testing "incremental checksum remains stable when a newly created block is retracted in the same batch"
(let [db0 (sample-db)
checksum0 (checksum/recompute-checksum db0)
page-id 1
parent-id 3
transient-uuid (random-uuid)
report-1 (d/with db0 [{:db/id -1
:block/uuid transient-uuid
:block/title "Transient"
:block/parent parent-id
:block/page page-id}])
db1 (:db-after report-1)
report-2 (d/with db1 [[:db/retractEntity [:block/uuid transient-uuid]]])
db2 (:db-after report-2)
batch-report {:db-before db0
:db-after db2
:tx-data (vec (concat (:tx-data report-1)
(:tx-data report-2)))}
full-final (checksum/recompute-checksum db2)
incremental (checksum/update-checksum checksum0 batch-report)]
(is (= checksum0 full-final))
(is (= full-final incremental)))))
(deftest incremental-checksum-handles-new-entity-attr-replacement-in-one-batch-test
(testing "incremental checksum cancels replaced attrs on entities created inside the same batch"
(let [db0 (sample-db)
checksum0 (checksum/recompute-checksum db0)
page-id 1
parent-id 3
transient-uuid (random-uuid)
report-1 (d/with db0 [{:db/id -1
:block/uuid transient-uuid
:block/title "Transient"
:block/order "a1"
:block/parent parent-id
:block/page page-id}])
db1 (:db-after report-1)
report-2 (d/with db1 [[:db/add [:block/uuid transient-uuid] :block/order "a2"]])
db2 (:db-after report-2)
batch-report {:db-before db0
:db-after db2
:tx-data (vec (concat (:tx-data report-1)
(:tx-data report-2)))}
full-final (checksum/recompute-checksum db2)
incremental (checksum/update-checksum checksum0 batch-report)]
(is (= full-final incremental)))))
(deftest incremental-checksum-matches-recompute-with-duplicate-block-uuid-tx-churn-test
(testing "incremental checksum stays equal to recompute when tx-data retracts+adds the same :block/uuid in one tx"
(let [db0 (sample-db)
shared-uuid (random-uuid)
db1 (:db-after (d/with db0 [{:db/id 5
:block/uuid shared-uuid
:block/title "Peer"
:block/parent 1
:block/page 1}]))
original-parent-uuid (:block/uuid (d/entity db1 3))
tx-data [(d/datom 3 :block/uuid original-parent-uuid 200 false)
(d/datom 5 :block/uuid shared-uuid 200 false)
(d/datom 3 :block/uuid shared-uuid 200 true)
(d/datom 3 :block/order "a9" 200 true)]
tx-report (d/with db1 tx-data)
db-before (:db-before tx-report)
checksum-before (checksum/recompute-checksum db-before)
full (checksum/recompute-checksum (:db-after tx-report))
incremental (checksum/update-checksum checksum-before tx-report)]
(is (= full incremental)))))
(deftest checksum-ignores-non-page-non-block-entities-test
(testing "entities with uuid but without page semantics do not affect checksum"
(let [db0 (sample-db)
checksum0 (checksum/recompute-checksum db0)
internal-uuid (random-uuid)
tx-report (d/with db0 [{:db/id -1
:block/uuid internal-uuid
:block/order "zz"}])
db1 (:db-after tx-report)
full1 (checksum/recompute-checksum db1)
incremental1 (checksum/update-checksum checksum0 tx-report)
tx-report-2 (d/with db1 [[:db/add [:block/uuid internal-uuid] :block/order "aa"]])
full2 (checksum/recompute-checksum (:db-after tx-report-2))
incremental2 (checksum/update-checksum full1 tx-report-2)]
(is (= checksum0 full1))
(is (= checksum0 incremental1))
(is (= checksum0 full2))
(is (= full2 incremental2)))))

View File

@@ -0,0 +1,91 @@
(ns logseq.db-sync.common-test
(:require [cljs.test :refer [async deftest is]]
[logseq.db-sync.common :as common]
[promesa.core :as p]))
(defn- make-stmt [calls result]
(let [stmt (js-obj)]
(aset stmt
"bind"
(fn [& args]
(swap! calls assoc :bind-args (vec args))
stmt))
(aset stmt
"all"
(fn []
(swap! calls update :all-count (fnil inc 0))
result))
stmt))
(deftest d1-all-falls-back-when-with-session-missing-test
(async done
(let [calls (atom {})
result #js {:results #js [#js {"id" "u1"}]}
stmt (make-stmt calls result)
db #js {:prepare (fn [sql]
(swap! calls assoc :prepare-sql sql)
stmt)}]
(-> (p/let [query-result (common/<d1-all db
"select id from users where id = ?"
"u1")]
(is (= result query-result))
(is (= "select id from users where id = ?"
(:prepare-sql @calls)))
(is (= ["u1"] (:bind-args @calls)))
(is (= 1 (:all-count @calls))))
(p/then (fn [] (done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest d1-all-uses-with-session-mode-when-provided-test
(async done
(let [calls (atom {})
result #js {:results #js [#js {"id" "u1"}]}
stmt (make-stmt calls result)
session #js {:prepare (fn [sql]
(swap! calls assoc :prepare-sql sql)
stmt)}
db #js {:prepare (fn [_]
(throw (js/Error. "db.prepare should not be called")))
:withSession (fn [& args]
(swap! calls assoc :with-session-args (vec args))
session)}]
(-> (p/let [query-result (common/<d1-all db
{:session "first-primary"}
"select id from users where id = ?"
"u1")]
(is (= result query-result))
(is (= ["first-primary"] (:with-session-args @calls)))
(is (= "select id from users where id = ?"
(:prepare-sql @calls)))
(is (= ["u1"] (:bind-args @calls)))
(is (= 1 (:all-count @calls))))
(p/then (fn [] (done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest d1-all-uses-default-with-session-when-no-mode-test
(async done
(let [calls (atom {})
result #js {:results #js []}
stmt (make-stmt calls result)
session #js {:prepare (fn [sql]
(swap! calls assoc :prepare-sql sql)
stmt)}
db #js {:prepare (fn [_]
(throw (js/Error. "db.prepare should not be called")))
:withSession (fn [& args]
(swap! calls assoc :with-session-args (vec args))
session)}]
(-> (p/let [query-result (common/<d1-all db "select 1")]
(is (= result query-result))
(is (= [] (:with-session-args @calls)))
(is (= "select 1" (:prepare-sql @calls)))
(is (nil? (:bind-args @calls)))
(is (= 1 (:all-count @calls))))
(p/then (fn [] (done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))

View File

@@ -0,0 +1,229 @@
(ns logseq.db-sync.cycle-test
(:require [cljs.test :refer [deftest is testing]]
[datascript.core :as d]
[logseq.db-sync.cycle :as cycle]
[logseq.db.test.helper :as db-test]))
(defn- new-conn []
(db-test/create-conn))
(defn- fix-cycle!
[temp-conn remote-tx-report rebase-tx-report]
(let [tx-metas (atom [])]
(cycle/fix-cycle! temp-conn remote-tx-report rebase-tx-report
{:transact! (fn [conn tx-data tx-meta]
(swap! tx-metas conj tx-meta)
(d/transact! conn tx-data tx-meta))})
@tx-metas))
(defn- create-page!
[conn title]
(let [page-uuid (random-uuid)]
(d/transact! conn [{:block/uuid page-uuid
:block/name title
:block/title title}])
page-uuid))
(defn- block-eid
[db uuid]
(d/entid db [:block/uuid uuid]))
(deftest block-parent-2-node-cycle-test
(let [conn (new-conn)
page-uuid (create-page! conn "page")
a (random-uuid)
b (random-uuid)]
(d/transact! conn [{:block/uuid a
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid page-uuid]}
{:block/uuid b
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid a]}])
(testing "breaks a 2-node :block/parent cycle and reparents to the page"
(let [remote-report (d/transact! conn [{:block/uuid a :block/parent [:block/uuid b]}])
tx-metas (fix-cycle! conn remote-report nil)
a' (d/entity @conn [:block/uuid a])
b' (d/entity @conn [:block/uuid b])]
(is (= (block-eid @conn page-uuid) (:db/id (:block/parent a'))))
(is (= (block-eid @conn a) (:db/id (:block/parent b'))))
(is (some #(= :fix-cycle (:outliner-op %)) tx-metas))
(is (every? #(false? (:gen-undo-ops? %)) tx-metas))
(is (every? #(false? (:persist-op? %)) tx-metas))))))
(deftest block-parent-3-node-cycle-test
(let [conn (new-conn)
page-uuid (create-page! conn "page")
a (random-uuid)
b (random-uuid)
c (random-uuid)]
(d/transact! conn [{:block/uuid a
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid page-uuid]}
{:block/uuid b
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid a]}
{:block/uuid c
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid b]}])
(testing "breaks a 3-node :block/parent cycle and reparents to the page"
(let [remote-report (d/transact! conn [{:block/uuid a :block/parent [:block/uuid c]}])
tx-metas (fix-cycle! conn remote-report nil)]
(let [a' (d/entity @conn [:block/uuid a])
b' (d/entity @conn [:block/uuid b])
c' (d/entity @conn [:block/uuid c])]
(is (= (block-eid @conn page-uuid) (:db/id (:block/parent a'))))
(is (= (block-eid @conn a) (:db/id (:block/parent b'))))
(is (= (block-eid @conn b) (:db/id (:block/parent c')))))
(is (some #(= :fix-cycle (:outliner-op %)) tx-metas))
(is (every? #(false? (:gen-undo-ops? %)) tx-metas))
(is (every? #(false? (:persist-op? %)) tx-metas))))))
(deftest block-parent-cycle-prefers-remote-parent-test
(let [conn (new-conn)
page-uuid (create-page! conn "page")
a (random-uuid)
b (random-uuid)
c (random-uuid)]
(d/transact! conn [{:block/uuid a
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid page-uuid]}
{:block/uuid b
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid page-uuid]}
{:block/uuid c
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid page-uuid]}])
(testing "prefers remote parent when breaking local cycle"
(let [remote-report (d/transact! conn [{:block/uuid b :block/parent [:block/uuid c]}
{:block/uuid a :block/parent [:block/uuid b]}])
rebase-report (d/transact! conn [{:block/uuid b :block/parent [:block/uuid a]}
{:block/uuid c :block/parent [:block/uuid b]}])
tx-metas (fix-cycle! conn remote-report rebase-report)
a' (d/entity @conn [:block/uuid a])
b' (d/entity @conn [:block/uuid b])
c' (d/entity @conn [:block/uuid c])]
(is (= (block-eid @conn b) (:db/id (:block/parent a'))))
(is (= (block-eid @conn c) (:db/id (:block/parent b'))))
(is (= (block-eid @conn page-uuid) (:db/id (:block/parent c'))))
(is (some #(= :fix-cycle (:outliner-op %)) tx-metas))
(is (every? #(false? (:gen-undo-ops? %)) tx-metas))
(is (every? #(false? (:persist-op? %)) tx-metas))))))
(deftest block-parent-cycle-avoids-descendant-remote-parent-test
(let [conn (new-conn)
page-uuid (create-page! conn "page")
a (random-uuid)
b (random-uuid)
c (random-uuid)]
(d/transact! conn [{:block/uuid a
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid page-uuid]}
{:block/uuid b
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid a]}
{:block/uuid c
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid b]}])
(testing "falls back to page when remote parent is a descendant"
(let [remote-report (d/transact! conn [{:block/uuid b :block/parent [:block/uuid c]}])
rebase-report (d/transact! conn [{:block/uuid a :block/parent [:block/uuid b]}])
tx-metas (fix-cycle! conn remote-report rebase-report)
a' (d/entity @conn [:block/uuid a])
b' (d/entity @conn [:block/uuid b])
c' (d/entity @conn [:block/uuid c])]
(is (= (block-eid @conn page-uuid) (:db/id (:block/parent b'))))
(is (= (block-eid @conn b) (:db/id (:block/parent a'))))
(is (= (block-eid @conn b) (:db/id (:block/parent c'))))
(is (some #(= :fix-cycle (:outliner-op %)) tx-metas))
(is (every? #(false? (:gen-undo-ops? %)) tx-metas))
(is (every? #(false? (:persist-op? %)) tx-metas))))))
(deftest block-parent-cycle-preserves-safe-remote-edges-test
(let [conn (new-conn)
page-uuid (create-page! conn "page")
a (random-uuid)
b (random-uuid)
c (random-uuid)]
(d/transact! conn [{:block/uuid a
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid page-uuid]}
{:block/uuid b
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid a]}
{:block/uuid c
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid b]}])
(testing "preserves remote parent when it doesn't reintroduce a cycle"
(let [remote-report (d/transact! conn [{:block/uuid b :block/parent [:block/uuid c]}
{:block/uuid a :block/parent [:block/uuid b]}])
rebase-report (d/transact! conn [{:block/uuid b :block/parent [:block/uuid a]}
{:block/uuid c :block/parent [:block/uuid b]}])
tx-metas (fix-cycle! conn remote-report rebase-report)
a' (d/entity @conn [:block/uuid a])
b' (d/entity @conn [:block/uuid b])
c' (d/entity @conn [:block/uuid c])]
(is (= (block-eid @conn b) (:db/id (:block/parent a'))))
(is (= (block-eid @conn page-uuid) (:db/id (:block/parent b'))))
(is (= (block-eid @conn b) (:db/id (:block/parent c'))))
(is (some #(= :fix-cycle (:outliner-op %)) tx-metas))
(is (every? #(false? (:gen-undo-ops? %)) tx-metas))
(is (every? #(false? (:persist-op? %)) tx-metas))))))
(deftest class-extends-2-node-cycle-test
(let [conn (new-conn)]
(d/transact! conn [{:db/ident :logseq.class/Root}
{:db/ident :user.class/B}
{:db/ident :user.class/A :logseq.property.class/extends :user.class/B}])
(testing "breaks a 2-node :logseq.property.class/extends cycle"
(let [remote-report (d/transact! conn [{:db/ident :user.class/B
:logseq.property.class/extends :user.class/A}])
tx-metas (fix-cycle! conn remote-report nil)]
(let [b (d/entity @conn :user.class/B)
extends (set (map :db/ident (:logseq.property.class/extends b)))]
(is (not (contains? extends :user.class/A)))
(is (contains? extends :logseq.class/Root)))
(is (some #(= :fix-cycle (:outliner-op %)) tx-metas))
(is (every? #(false? (:gen-undo-ops? %)) tx-metas))
(is (every? #(false? (:persist-op? %)) tx-metas))))))
(deftest class-extends-3-node-cycle-with-multiple-values-test
(let [conn (new-conn)]
(d/transact! conn [{:db/ident :logseq.class/Root}
{:db/ident :user.class/C}
{:db/ident :user.class/D}
{:db/ident :user.class/B :logseq.property.class/extends :user.class/C}
{:db/ident :user.class/A :logseq.property.class/extends :user.class/B}])
(testing "breaks a 3-node :logseq.property.class/extends cycle while preserving other extends"
(let [remote-report (d/transact! conn [{:db/ident :user.class/C
:logseq.property.class/extends #{:user.class/A :user.class/D}}])
tx-metas (fix-cycle! conn remote-report nil)]
(let [c (d/entity @conn :user.class/C)
extends (set (map :db/ident (:logseq.property.class/extends c)))]
(is (not (contains? extends :user.class/A)))
(is (contains? extends :user.class/D))
(is (contains? extends :logseq.class/Root)))
(is (some #(= :fix-cycle (:outliner-op %)) tx-metas))
(is (every? #(false? (:gen-undo-ops? %)) tx-metas))
(is (every? #(false? (:persist-op? %)) tx-metas))))))
(deftest class-extends-cycle-prefers-remote-set-test
(let [conn (new-conn)]
(d/transact! conn [{:db/ident :logseq.class/Root}
{:db/ident :user.class/B}
{:db/ident :user.class/A :logseq.property.class/extends :logseq.class/Root}])
(testing "prefers remote extends set when breaking cycles"
(let [remote-report (d/transact! conn [{:db/ident :user.class/B
:logseq.property.class/extends :user.class/A}])
rebase-report (d/transact! conn [{:db/ident :user.class/A
:logseq.property.class/extends :user.class/B}])
tx-metas (fix-cycle! conn remote-report rebase-report)]
(let [a (d/entity @conn :user.class/A)
b (d/entity @conn :user.class/B)
extends-a (set (map :db/ident (:logseq.property.class/extends a)))
extends-b (set (map :db/ident (:logseq.property.class/extends b)))]
(is (not (contains? extends-a :user.class/B)))
(is (contains? extends-a :logseq.class/Root))
(is (contains? extends-b :user.class/A)))
(is (some #(= :fix-cycle (:outliner-op %)) tx-metas))
(is (every? #(false? (:gen-undo-ops? %)) tx-metas))
(is (every? #(false? (:persist-op? %)) tx-metas))))))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,197 @@
(ns logseq.db-sync.index-test
(:require [cljs.test :refer [async deftest is]]
[clojure.string :as string]
[logseq.db-sync.common :as common]
[logseq.db-sync.index :as index]
[promesa.core :as p]))
(def ^:private graph-e2ee-migration-sql
"alter table graphs add column graph_e2ee integer default 1")
(def ^:private graph-ready-for-use-migration-sql
"alter table graphs add column graph_ready_for_use integer default 1")
(def ^:private graph-members-graph-id-created-at-index-sql
"create index if not exists idx_graph_members_graph_id_created_at on graph_members (graph_id, created_at)")
(def ^:private graphs-user-id-updated-at-index-sql
"create index if not exists idx_graphs_user_id_updated_at on graphs (user_id, updated_at desc)")
(def ^:private users-email-index-sql
"create index if not exists idx_users_email on users (email)")
(deftest index-list-includes-graph-e2ee-flag-test
(async done
(let [rows #js [#js {"graph_id" "graph-1"
"graph_name" "Graph 1"
"schema_version" "65"
"role" "manager"
"invited_by" nil
"created_at" 10
"updated_at" 20
"graph_e2ee" 0}
#js {"graph_id" "graph-2"
"graph_name" "Graph 2"
"schema_version" "65"
"role" "member"
"invited_by" "u1"
"created_at" 11
"updated_at" 21
"graph_e2ee" 1
"graph_ready_for_use" 0}]]
(-> (p/with-redefs [common/<d1-all (fn [& _]
(p/resolved #js {:results rows}))
common/get-sql-rows (fn [result]
(aget result "results"))]
(index/<index-list :db "user-1"))
(p/then (fn [graphs]
(is (= 2 (count graphs)))
(is (= false (:graph-e2ee? (first graphs))))
(is (= true (:graph-e2ee? (second graphs))))
(is (= true (:graph-ready-for-use? (first graphs))))
(is (= false (:graph-ready-for-use? (second graphs))))
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest semantic-graphs-list-is-bounded-and-excludes-e2ee-test
(async done
(let [call (atom nil)
rows #js [#js {"graph_id" "graph-1" "graph_name" "test-mcp"
"updated_at" 20 "created_at" 10 "schema_version" "65"
"graph_ready_for_use" 1 "role" "manager" "invited_by" nil}
#js {"graph_id" "graph-2" "graph_name" "test-mcp-2"
"updated_at" 19 "created_at" 11 "schema_version" "65"
"graph_ready_for_use" 1 "role" "member" "invited_by" "u2"}]]
(-> (p/with-redefs [common/<d1-all
(fn [_db sql & args]
(reset! call {:sql sql :args args})
(p/resolved #js {:results rows}))
common/get-sql-rows (fn [result] (aget result "results"))]
(index/<semantic-graphs-list :db "user-1" {:name "test-mcp" :limit 1 :cursor nil}))
(p/then (fn [{:keys [graphs next-cursor]}]
(is (= 1 (count graphs)))
(is (string? next-cursor))
(is (string/includes? (:sql @call) "g.graph_e2ee = 0"))
(is (string/includes? (:sql @call) "limit ?"))
(is (= 2 (last (:args @call))))
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest index-upsert-persists-graph-e2ee-flag-test
(async done
(let [called (atom nil)]
(-> (p/with-redefs [common/now-ms (fn [] 1234)
common/<d1-run (fn [_db sql & args]
(reset! called {:sql sql
:args args})
(p/resolved {:ok true}))]
(index/<index-upsert! :db "graph-1" "Graph 1" "user-1" "65" false))
(p/then (fn [_]
(is (string/includes? (:sql @called) "graph_e2ee"))
(is (= ["graph-1" "Graph 1" "user-1" "65" 0 1 1234 1234]
(:args @called)))
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest index-upsert-persists-graph-ready-for-use-flag-test
(async done
(let [called (atom nil)]
(-> (p/with-redefs [common/now-ms (fn [] 1234)
common/<d1-run (fn [_db sql & args]
(reset! called {:sql sql
:args args})
(p/resolved {:ok true}))]
(index/<index-upsert! :db "graph-1" "Graph 1" "user-1" "65" false false))
(p/then (fn [_]
(is (string/includes? (:sql @called) "graph_ready_for_use"))
(is (= ["graph-1" "Graph 1" "user-1" "65" 0 0 1234 1234]
(:args @called)))
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest index-init-runs-graph-e2ee-migration-test
(async done
(let [sql-calls (atom [])]
(-> (p/with-redefs [common/<d1-all (fn [& _]
(p/resolved #js {:results #js []}))
common/get-sql-rows (fn [result]
(aget result "results"))
common/<d1-run (fn [_db sql & _args]
(swap! sql-calls conj (string/lower-case sql))
(p/resolved {:ok true}))]
(index/<index-init! :db))
(p/then (fn [_]
(is (some #(string/includes? % graph-e2ee-migration-sql)
@sql-calls))
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest index-init-runs-graph-ready-for-use-migration-test
(async done
(let [sql-calls (atom [])]
(-> (p/with-redefs [common/<d1-all (fn [& _]
(p/resolved #js {:results #js []}))
common/get-sql-rows (fn [result]
(aget result "results"))
common/<d1-run (fn [_db sql & _args]
(swap! sql-calls conj (string/lower-case sql))
(p/resolved {:ok true}))]
(index/<index-init! :db))
(p/then (fn [_]
(is (some #(string/includes? % graph-ready-for-use-migration-sql)
@sql-calls))
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest index-init-ignores-duplicate-graph-e2ee-column-error-test
(async done
(let [sql-calls (atom [])]
(-> (p/with-redefs [common/<d1-all (fn [& _]
(p/resolved #js {:results #js []}))
common/get-sql-rows (fn [result]
(aget result "results"))
common/<d1-run (fn [_db sql & _args]
(let [sql' (string/lower-case sql)]
(swap! sql-calls conj sql')
(if (string/includes? sql' graph-e2ee-migration-sql)
(p/rejected (ex-info "duplicate column name: graph_e2ee" {}))
(p/resolved {:ok true}))))]
(index/<index-init! :db))
(p/then (fn [_]
(is (some #(string/includes? % graph-e2ee-migration-sql)
@sql-calls))
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest index-init-creates-indexes-test
(async done
(let [sql-calls (atom [])]
(-> (p/with-redefs [common/<d1-all (fn [& _]
(p/resolved #js {:results #js []}))
common/get-sql-rows (fn [result]
(aget result "results"))
common/<d1-run (fn [_db sql & _args]
(swap! sql-calls conj (string/lower-case sql))
(p/resolved {:ok true}))]
(index/<index-init! :db))
(p/then (fn [_]
(is (some #(string/includes? % graph-members-graph-id-created-at-index-sql)
@sql-calls))
(is (some #(string/includes? % graphs-user-id-updated-at-index-sql)
@sql-calls))
(is (some #(string/includes? % users-email-index-sql)
@sql-calls))
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))

View File

@@ -0,0 +1,42 @@
(ns logseq.db-sync.malli-schema-test
(:require [cljs.test :refer [deftest is testing]]
[logseq.db-sync.malli-schema :as db-sync-schema]))
(def ^:private request-samples
{:graphs/create {:graph-name "Demo"}
:graph-members/create {:email "user@example.com"}
:graph-members/update {:role "member"}
:sync/tx-batch {:t-before 0 :txs []}
:e2ee/user-keys {:public-key "public"
:encrypted-private-key "private"}
:e2ee/graph-aes-key {:encrypted-aes-key "aes"}
:e2ee/grant-access {:target-user-email+encrypted-aes-key-coll
[{:email "user@example.com"
:encrypted-aes-key "aes"}]}})
(defn- coerce-request
[schema-key body]
((get db-sync-schema/http-request-coercers schema-key) body))
(deftest http-request-client-revision-is-optional-test
(doseq [[schema-key body] request-samples]
(testing schema-key
(is (= body (coerce-request schema-key body))))))
(deftest tx-batch-request-client-revision-accepts-string-test
(let [body' (assoc (:sync/tx-batch request-samples)
:client-revision "test-revision")]
(is (= body' (coerce-request :sync/tx-batch body')))))
(deftest ws-tx-batch-client-revision-accepts-string-test
(let [body {:type "tx/batch"
:t-before 0
:txs []
:client-revision "test-revision"}]
(is (= body (db-sync-schema/ws-client-message-coercer body)))))
(deftest tx-batch-request-client-revision-rejects-non-string-test
(is (thrown? js/Error
(coerce-request :sync/tx-batch
(assoc (:sync/tx-batch request-samples)
:client-revision 42)))))

View File

@@ -0,0 +1,108 @@
(ns logseq.db-sync.node-adapter-test
;; Linters disabled because commented out FIXME code causes false positives
{:clj-kondo/config {:ignore true}}
(:require [clojure.string :as string]
[logseq.db-sync.node.server :as node-server]
[logseq.db-sync.protocol :as protocol]
[promesa.core :as p]))
(def test-token "test-token")
(defn- auth-headers []
#js {"authorization" (str "Bearer " test-token)
"content-type" "application/json"})
(defn- post-json [url body]
(js/fetch url #js {:method "POST"
:headers (auth-headers)
:body (js/JSON.stringify (clj->js body))}))
(defn- get-json [url]
(js/fetch url #js {:method "GET" :headers (auth-headers)}))
(defn- parse-json [resp]
(.json resp))
(defn- start-test-server []
(let [suffix (str (random-uuid))
dir (str "tmp/db-sync-node-test/" suffix)]
(node-server/start! {:port 0
:data-dir dir})))
;; FIXME: Tests are disabled until they stop hanging
#_(deftest node-adapter-http-roundtrip-test
(async done
(p/let [{:keys [base-url stop!]} (start-test-server)
health-resp (js/fetch (str base-url "/health"))
health-body (parse-json health-resp)]
(testing "health"
(is (.-ok health-resp))
(is (= true (aget health-body "ok"))))
;; FIXME: Test hangs here due to an exception
(p/let [create-resp (post-json (str base-url "/graphs") {:graph-name "Test Graph"})
create-body (parse-json create-resp)
graph-id (aget create-body "graph-id")
access-resp (get-json (str base-url "/graphs/" graph-id "/access"))
access-body (parse-json access-resp)
sync-health (get-json (str base-url "/sync/" graph-id "/health"))
sync-health-body (parse-json sync-health)]
(testing "graph access"
(is (.-ok create-resp))
(is (string? graph-id))
(is (.-ok access-resp))
(is (= true (aget access-body "ok"))))
(testing "sync health"
(is (.-ok sync-health))
(is (= true (aget sync-health-body "ok"))))
(p/let [tx-data [{:block/uuid (random-uuid)
:block/title "hello"}]
tx-entry {:tx (protocol/tx->transit tx-data)
:outliner-op :save-block}
tx-resp (post-json (str base-url "/sync/" graph-id "/tx/batch")
{:t-before 0
:txs [tx-entry]})
tx-body (parse-json tx-resp)
pull-resp (get-json (str base-url "/sync/" graph-id "/pull?since=0"))
pull-body (parse-json pull-resp)]
(testing "tx batch"
(is (.-ok tx-resp))
(is (= "tx/batch/ok" (aget tx-body "type"))))
(testing "pull"
(is (.-ok pull-resp))
(is (= "pull/ok" (aget pull-body "type")))
(is (pos? (count (aget pull-body "txs"))))
(is (= "save-block"
(aget (aget pull-body "txs" 0) "outliner-op"))))
(p/then (stop!) (fn [] (done))))))))
#_(deftest node-adapter-websocket-test
(async done
(p/let [{:keys [base-url stop!]} (start-test-server)
create-resp (post-json (str base-url "/graphs") {:graph-name "WS Graph"})
create-body (parse-json create-resp)
graph-id (aget create-body "graph-id")]
(testing "websocket hello and changed"
(let [ws-url (str (string/replace base-url "http" "ws") "/sync/" graph-id)
ws-module (js/require "ws")
WebSocket (or (.-WebSocket ws-module) ws-module)
client (new WebSocket ws-url #js {:headers (auth-headers)})
messages (atom [])
push-message (fn [data]
(let [text (if (string? data) data (.toString data))]
(swap! messages conj (js/JSON.parse text))))]
(.on client "message" push-message)
(.on client "open"
(fn []
(.send client (protocol/encode-message {:type "hello" :client "test"}))))
(p/let [_ (p/delay 50)
tx-data [{:block/uuid (random-uuid)
:block/title "ws"}]
txs (protocol/tx->transit tx-data)
_ (post-json (str base-url "/sync/" graph-id "/tx/batch")
{:t-before 0
:txs txs})
_ (p/delay 100)]
(let [types (set (map #(aget % "type") @messages))]
(is (contains? types "hello"))
(is (contains? types "changed")))
(.close client)
(p/then (stop!) (fn [] (done)))))))))

View File

@@ -0,0 +1,31 @@
(ns logseq.db-sync.node-config-test
(:require [cljs.test :refer [deftest is testing]]
[logseq.db-sync.node.config :as config]))
(defn- throws? [f]
(try
(f)
false
(catch :default _ true)))
(deftest normalize-config-drops-unknown-keys-test
(let [cfg (config/normalize-config {:port 7777
:unknown-key "value"
:legacy-auth-key "value"})]
(is (= 7777 (:port cfg)))
(is (nil? (:unknown-key cfg)))
(is (nil? (:legacy-auth-key cfg)))))
(deftest normalize-config-storage-driver-test
(testing "sqlite storage driver accepted"
(let [cfg (config/normalize-config {:storage-driver "sqlite"})]
(is (= "sqlite" (:storage-driver cfg)))))
(testing "unsupported storage driver throws"
(is (throws? #(config/normalize-config {:storage-driver "other"})))))
(deftest normalize-config-assets-driver-test
(testing "filesystem assets driver accepted"
(let [cfg (config/normalize-config {:assets-driver "filesystem"})]
(is (= "filesystem" (:assets-driver cfg)))))
(testing "unsupported assets driver throws"
(is (throws? #(config/normalize-config {:assets-driver "s3"})))))

View File

@@ -0,0 +1,163 @@
(ns logseq.db-sync.node-server-test
(:require [cljs.test :refer [async deftest is]]
[logseq.db-sync.node.server :as node-server]
[logseq.db-sync.platform.node :as platform-node]
[logseq.db-sync.worker.auth :as auth]
[promesa.core :as p]))
(defn- fetch-with-timeout [url timeout-ms]
(let [timeout-sentinel ::timeout]
{:sentinel timeout-sentinel
:promise (js/Promise.
(fn [resolve reject]
(let [controller (js/AbortController.)
timeout-id (js/setTimeout
(fn []
(.abort controller))
timeout-ms)]
(-> (js/fetch url #js {:method "GET"
:headers #js {"authorization" "Bearer test.token.sig"}
:signal (.-signal controller)})
(.then (fn [response]
(js/clearTimeout timeout-id)
(resolve response)))
(.catch (fn [error]
(js/clearTimeout timeout-id)
(if (= "AbortError" (.-name error))
(resolve timeout-sentinel)
(reject error))))))))}))
(deftest node-server-returns-500-when-auth-claims-rejects-test
(async done
(let [stop-server! (atom nil)
test-url (atom nil)]
(-> (p/with-redefs [auth/auth-claims
(fn [_request _env]
(p/rejected (ex-info "jwks" {})))]
(p/let [{:keys [base-url stop!]} (node-server/start! {:port 0
:data-dir (str "tmp/db-sync-node-server-test/" (random-uuid))})
_ (reset! stop-server! stop!)
_ (reset! test-url (str base-url "/graphs"))
{:keys [promise sentinel]} (fetch-with-timeout @test-url 1200)
response promise]
(if (identical? response sentinel)
(is false "request timed out")
(p/let [body (.json response)]
(is (= 500 (.-status response)))
(is (= "server error" (aget body "error")))))))
(p/then
(fn []
(if-let [stop! @stop-server!]
(-> (stop!)
(p/then (fn [] (done)))
(p/catch (fn [error]
(is false (str error))
(done))))
(done))))
(p/catch
(fn [error]
(if-let [stop! @stop-server!]
(-> (stop!)
(p/then (fn []
(is false (str error))
(done)))
(p/catch (fn [stop-error]
(is false (str error))
(is false (str stop-error))
(done))))
(do
(is false (str error))
(done)))))))))
(deftest node-server-request-origin-uses-configured-base-url-host-test
(async done
(let [stop-server! (atom nil)
request-opts (atom [])
original-request-from-node platform-node/request-from-node]
(-> (p/with-redefs [platform-node/request-from-node
(fn [req opts]
(swap! request-opts conj opts)
(original-request-from-node req opts))]
(p/let [{:keys [port stop!]} (node-server/start! {:port 0
:base-url "https://sync.example.test:9443"
:data-dir (str "tmp/db-sync-node-server-base-url-test/" (random-uuid))})
_ (reset! stop-server! stop!)
response (js/fetch (str "http://localhost:" port "/health"))]
(is (= 200 (.-status response)))
(is (some #(= {:scheme "https"
:host "sync.example.test:9443"}
%)
@request-opts))))
(p/then
(fn []
(if-let [stop! @stop-server!]
(-> (stop!)
(p/then (fn [] (done)))
(p/catch (fn [error]
(is false (str error))
(done))))
(done))))
(p/catch
(fn [error]
(if-let [stop! @stop-server!]
(-> (stop!)
(p/then (fn []
(is false (str error))
(done)))
(p/catch (fn [stop-error]
(is false (str error))
(is false (str stop-error))
(done))))
(do
(is false (str error))
(done)))))))))
(deftest node-server-logs-request-failed-marker-when-auth-claims-rejects-test
(async done
(let [stop-server! (atom nil)
original-console-error (.-error js/console)
logged-errors (atom [])]
(aset js/console
"error"
(fn [& args]
(swap! logged-errors conj args)))
(-> (p/with-redefs [auth/auth-claims
(fn [_request _env]
(p/rejected (ex-info "jwks" {})))]
(p/let [{:keys [base-url stop!]} (node-server/start! {:port 0
:data-dir (str "tmp/db-sync-node-server-log-test/" (random-uuid))})
_ (reset! stop-server! stop!)
_ (reset! logged-errors [])
{:keys [promise sentinel]} (fetch-with-timeout (str base-url "/graphs") 1200)
response promise]
(if (identical? response sentinel)
(is false "request timed out")
(do
(is (= 500 (.-status response)))
(is (some #(= ":db-sync/node-request-failed" (first %))
@logged-errors))))))
(p/then
(fn []
(aset js/console "error" original-console-error)
(if-let [stop! @stop-server!]
(-> (stop!)
(p/then (fn [] (done)))
(p/catch (fn [error]
(is false (str error))
(done))))
(done))))
(p/catch
(fn [error]
(aset js/console "error" original-console-error)
(if-let [stop! @stop-server!]
(-> (stop!)
(p/then (fn []
(is false (str error))
(done)))
(p/catch (fn [stop-error]
(is false (str error))
(is false (str stop-error))
(done))))
(do
(is false (str error))
(done)))))))))

View File

@@ -0,0 +1,382 @@
(ns logseq.db-sync.normalize-test
(:require [cljs.test :refer [deftest is testing]]
[datascript.core :as d]
[logseq.db :as ldb]
[logseq.db.common.normalize :as db-normalize]
[logseq.db.test.helper :as db-test]))
(defn- new-conn []
(db-test/create-conn))
(defn- create-page!
[conn title]
(let [page-uuid (random-uuid)]
(d/transact! conn [{:block/uuid page-uuid
:block/name title
:block/title title}])
page-uuid))
(defn- op-e-a-v
[datom]
(subvec (vec datom) 0 4))
(defn- tx-touches-uuid?
[tx-data block-uuid]
(let [lookup [:block/uuid block-uuid]
block-uuid-str (str block-uuid)]
(boolean
(some (fn [item]
(let [item (vec item)]
(case (count item)
5 (or (= lookup (nth item 1))
(= block-uuid-str (nth item 1))
(= block-uuid (nth item 3)))
2 (or (= lookup (second item))
(= block-uuid-str (second item)))
false)))
tx-data))))
(defn- seeded-rng
[seed0]
(let [state (atom (bit-or (long seed0) 0))]
(fn []
(let [s (swap! state
(fn [x]
(let [x (bit-xor x (bit-shift-left x 13))
x (bit-xor x (bit-shift-right x 17))
x (bit-xor x (bit-shift-left x 5))]
(bit-or x 0))))]
(/ (double (unsigned-bit-shift-right s 0)) 4294967296.0)))))
(defn- rand-int*
[rng n]
(js/Math.floor (* (rng) n)))
(defn- pick-rand
[rng coll]
(when (seq coll)
(nth coll (rand-int* rng (count coll)))))
(defn- normal-block-uuids
[db]
(->> (d/datoms db :avet :block/uuid)
(map :e)
distinct
(keep (fn [eid]
(let [ent (d/entity db eid)]
(when (and (uuid? (:block/uuid ent))
(not (ldb/built-in? ent))
(nil? (:block/name ent))
(some? (:block/page ent)))
(:block/uuid ent)))))
vec))
(defn- block-state
[db]
(->> (d/datoms db :avet :block/uuid)
(map :e)
distinct
(keep (fn [eid]
(let [ent (d/entity db eid)]
(when (and (uuid? (:block/uuid ent))
(not (ldb/built-in? ent))
(or (string? (:block/name ent))
(some? (:block/page ent))))
[(:block/uuid ent)
{:block/uuid (:block/uuid ent)
:block/name (:block/name ent)
:block/title (:block/title ent)
:block/page (some-> ent :block/page :block/uuid)
:block/parent (some-> ent :block/parent :block/uuid)
:block/order (:block/order ent)}]))))
(into (sorted-map))))
(deftest normalize-tx-data-keeps-title-retract-without-replacement-test
(let [conn (new-conn)
page-uuid (create-page! conn "Page")
tx-report (d/transact! conn [[:db/retract [:block/uuid page-uuid] :block/title "Page"]])
normalized (db-normalize/normalize-tx-data (:db-after tx-report)
(:db-before tx-report)
(:tx-data tx-report))
tx-data (mapv op-e-a-v normalized)]
(testing "keeps :block/title retract when no replacement title exists in same tx"
(is (= [[:db/retract [:block/uuid page-uuid] :block/title "Page"]]
tx-data)))))
(deftest normalize-tx-data-drops-title-retract-when-replaced-test
(let [conn (new-conn)
page-uuid (create-page! conn "Page")
tx-report (d/transact! conn [{:block/uuid page-uuid
:block/title "Page 2"}])
normalized (db-normalize/normalize-tx-data (:db-after tx-report)
(:db-before tx-report)
(:tx-data tx-report))
tx-data (mapv op-e-a-v normalized)]
(testing "keeps old :block/title retract and new add during title update"
(is (some #(= [:db/add [:block/uuid page-uuid] :block/title "Page 2"] %) tx-data))
(is (some #(= [:db/retract [:block/uuid page-uuid] :block/title "Page"] %) tx-data)))))
(deftest normalize-tx-data-keeps-recreated-normal-blocks-test
(testing "retract + recreate for normal blocks should not drop recreated entity datoms"
(let [conn (new-conn)
page-uuid (create-page! conn "Page")
target-uuid (random-uuid)
sibling-uuid (random-uuid)
child-uuid (random-uuid)
_ (d/transact! conn [{:block/uuid target-uuid
:block/title "old-target"
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid page-uuid]
:block/order "a0"}
{:block/uuid sibling-uuid
:block/title "sibling"
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid page-uuid]
:block/order "a1"}
{:block/uuid child-uuid
:block/title "child"
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid target-uuid]
:block/order "a0"}])
db-before @conn
;; Shape A: retract then recreate with tempid + explicit :block/uuid add.
tx-report-a (d/with db-before
[[:db/retractEntity [:block/uuid target-uuid]]
[:db/add -1 :block/uuid target-uuid]
[:db/add -1 :block/title "new-target-a"]
[:db/add -1 :block/page [:block/uuid page-uuid]]
[:db/add -1 :block/parent [:block/uuid page-uuid]]
[:db/add -1 :block/order "a0"]
[:db/add [:block/uuid sibling-uuid] :block/title "sibling-2"]]
{})
normalized-a (db-normalize/normalize-tx-data (:db-after tx-report-a)
(:db-before tx-report-a)
(:tx-data tx-report-a))
;; Shape B: same recreate, plus child reparent to recreated target.
tx-report-b (d/with db-before
[[:db/retractEntity [:block/uuid target-uuid]]
[:db/add -1 :block/uuid target-uuid]
[:db/add -1 :block/title "new-target-b"]
[:db/add -1 :block/page [:block/uuid page-uuid]]
[:db/add -1 :block/parent [:block/uuid page-uuid]]
[:db/add -1 :block/order "a2"]
[:db/add [:block/uuid child-uuid] :block/parent [:block/uuid target-uuid]]
[:db/add [:block/uuid sibling-uuid] :block/title "sibling-3"]]
{})
normalized-b (db-normalize/normalize-tx-data (:db-after tx-report-b)
(:db-before tx-report-b)
(:tx-data tx-report-b))]
(is (some? (d/entity (:db-after tx-report-a) [:block/uuid target-uuid])))
(is (some? (d/entity (:db-after tx-report-b) [:block/uuid target-uuid])))
(is (tx-touches-uuid? normalized-a target-uuid)
(str "shape A unexpectedly dropped recreated block from normalized tx: " (pr-str normalized-a)))
(is (tx-touches-uuid? normalized-b target-uuid)
(str "shape B unexpectedly dropped recreated block from normalized tx: " (pr-str normalized-b))))))
(deftest apply-client-delete-blocks-removes-current-server-subtree-test
(testing "a delete-blocks tx deletes children that exist on the server but not in the client tx"
(let [conn (new-conn)
page-uuid (create-page! conn "Page")
parent-uuid (random-uuid)
child-uuid (random-uuid)
now (js/Date.now)
_ (d/transact! conn [{:block/uuid parent-uuid
:block/title "parent"
:block/created-at now
:block/updated-at now
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid page-uuid]
:block/order "a0"}
{:block/uuid child-uuid
:block/title "child"
:block/created-at now
:block/updated-at now
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid parent-uuid]
:block/order "a0"}])
result (try
(ldb/transact! conn
[[:db/retractEntity [:block/uuid parent-uuid]]]
{:op :apply-client-tx
:outliner-op :delete-blocks})
:ok
(catch :default e
{:message (.-message e)
:data (ex-data e)}))]
(is (= :ok result))
(is (nil? (d/entity @conn [:block/uuid parent-uuid])))
(is (nil? (d/entity @conn [:block/uuid child-uuid]))))))
(deftest normalize-delete-blocks-keeps-property-history-retract-test
(testing "a normalized delete-blocks tx still removes property history rows for the deleted block"
(let [conn (new-conn)
page-uuid (create-page! conn "Page")
block-uuid (random-uuid)
history-uuid (random-uuid)
property-uuid (random-uuid)
value-uuid (random-uuid)
now (js/Date.now)]
(d/transact! conn [{:block/uuid property-uuid
:block/name "p"
:block/title "P"
:db/ident :user.property/p
:db/valueType :db.type/ref
:db/cardinality :db.cardinality/one
:logseq.property/type :default}
{:block/uuid value-uuid
:block/title "value"
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid page-uuid]
:block/order "a0"}
{:block/uuid block-uuid
:block/title "target"
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid page-uuid]
:block/order "a1"
:block/created-at now
:block/updated-at now}
{:block/uuid history-uuid
:block/created-at now
:block/updated-at now
:logseq.property.history/block [:block/uuid block-uuid]
:logseq.property.history/property :user.property/p
:logseq.property.history/ref-value [:block/uuid value-uuid]}])
(let [client-conn (d/conn-from-db @conn)
tx-report (ldb/transact! conn
[[:db/retractEntity [:block/uuid block-uuid]]]
{:outliner-op :delete-blocks})
normalized (db-normalize/normalize-tx-data (:db-after tx-report)
(:db-before tx-report)
(:tx-data tx-report))]
(is (some #(= [:db/retractEntity [:block/uuid history-uuid]] %) normalized))
(ldb/transact! client-conn normalized {:transact-remote? true
:outliner-op :delete-blocks})
(is (nil? (d/entity @client-conn [:block/uuid block-uuid])))
(is (nil? (d/entity @client-conn [:block/uuid history-uuid])))))))
(deftest remote-delete-blocks-history-ref-retract-deletes-history-entity-test
(testing "remote delete-blocks tx fully removes property history rows when their history block ref is retracted"
(let [conn (new-conn)
page-uuid (create-page! conn "Page")
block-uuid (random-uuid)
history-uuid (random-uuid)
property-uuid (random-uuid)
value-uuid (random-uuid)
now (js/Date.now)]
(d/transact! conn [{:block/uuid property-uuid
:block/name "p"
:block/title "P"
:db/ident :user.property/p
:db/valueType :db.type/ref
:db/cardinality :db.cardinality/one
:logseq.property/type :default}
{:block/uuid value-uuid
:block/title "value"
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid page-uuid]
:block/order "a0"}
{:block/uuid block-uuid
:block/title "target"
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid page-uuid]
:block/order "a1"
:block/created-at now
:block/updated-at now}
{:block/uuid history-uuid
:block/created-at now
:block/updated-at now
:logseq.property.history/block [:block/uuid block-uuid]
:logseq.property.history/property :user.property/p
:logseq.property.history/ref-value [:block/uuid value-uuid]}])
(ldb/transact! conn
[[:db/retract
[:block/uuid history-uuid]
:logseq.property.history/block
[:block/uuid block-uuid]]]
{:transact-remote? true
:outliner-op :delete-blocks})
(is (nil? (d/entity @conn [:block/uuid history-uuid]))))))
(deftest normalize-tx-data-replay-equivalence-for-retract-recreate-fuzz-test
(testing "normalized tx-data should replay to same db-after for normal-block retract/recreate patterns"
(let [conn (new-conn)
page-uuid (create-page! conn "Page")]
(d/transact! conn (mapv (fn [idx]
{:block/uuid (random-uuid)
:block/title (str "seed-block-" idx)
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid page-uuid]
:block/order (str "a" idx)})
(range 5)))
(dotimes [seed 300]
(let [rng (seeded-rng (+ 777 seed))
db-before @conn
blocks (normal-block-uuids db-before)
target (pick-rand rng blocks)
sibling (pick-rand rng (remove #(= % target) blocks))
move-candidate (pick-rand rng (remove #(= % target) blocks))
case-id (mod seed 6)
base [[:db/retractEntity [:block/uuid target]]
[:db/add -1 :block/uuid target]
[:db/add -1 :block/title (str "fuzz-target-" seed)]
[:db/add -1 :block/page [:block/uuid page-uuid]]
[:db/add -1 :block/parent [:block/uuid page-uuid]]
[:db/add -1 :block/order (str "z" (mod seed 5))]]
tx-data (case case-id
0
(cond-> base
sibling (conj [:db/add [:block/uuid sibling] :block/title (str "fuzz-sibling-title-" seed)]))
1
(cond-> base
sibling (conj [:db/retractEntity [:block/uuid sibling]]))
2
(if sibling
(into base
[[:db/retractEntity [:block/uuid sibling]]
[:db/add -2 :block/uuid sibling]
[:db/add -2 :block/title (str "fuzz-sibling-recreated-" seed)]
[:db/add -2 :block/page [:block/uuid page-uuid]]
[:db/add -2 :block/parent [:block/uuid page-uuid]]
[:db/add -2 :block/order (str "y" (mod seed 5))]])
base)
3
(into base
[[:db/add -2 :block/uuid (random-uuid)]
[:db/add -2 :block/title (str "fuzz-fresh-" seed)]
[:db/add -2 :block/page [:block/uuid page-uuid]]
[:db/add -2 :block/parent [:block/uuid page-uuid]]
[:db/add -2 :block/order (str "x" (mod seed 7))]])
4
(if sibling
(into base
[[:db/retractEntity [:block/uuid sibling]]
[:db/add -2 :block/uuid sibling]
[:db/add -2 :block/title (str "fuzz-sibling-reparented-" seed)]
[:db/add -2 :block/page [:block/uuid page-uuid]]
[:db/add -2 :block/parent [:block/uuid target]]
[:db/add -2 :block/order "a0"]])
base)
;; 5
(cond-> base
move-candidate
(conj [:db/add [:block/uuid move-candidate] :block/parent [:block/uuid target]])))
tx-report (d/with db-before tx-data {})
normalized (db-normalize/normalize-tx-data (:db-after tx-report)
(:db-before tx-report)
(:tx-data tx-report))
replay-report (d/with db-before normalized {})
expected (block-state (:db-after tx-report))
actual (block-state (:db-after replay-report))]
(is (= expected actual)
(str "seed=" seed
"\noriginal=" (pr-str tx-data)
"\nnormalized=" (pr-str normalized)
"\nexpected-count=" (count expected)
" actual-count=" (count actual))))))))

View File

@@ -0,0 +1,272 @@
(ns logseq.db-sync.storage-test
(:require ["better-sqlite3" :as sqlite3]
[clojure.string :as string]
[cljs.test :refer [deftest is testing]]
[datascript.core :as d]
[logseq.db :as ldb]
[logseq.db-sync.common :as common]
[logseq.db-sync.storage :as storage]
[logseq.db.common.normalize :as db-normalize]
[logseq.db-sync.test-sql :as test-sql]))
(def sqlite (if (find-ns 'nbb.core) (aget sqlite3 "default") sqlite3))
(defn- select-sql?
[sql]
(string/starts-with? (-> sql string/trim string/lower-case) "select"))
(defn- run-sql
[^js stmt args]
(.apply (.-run stmt) stmt (to-array args)))
(defn- all-sql
[^js stmt args]
(.apply (.-all stmt) stmt (to-array args)))
(defn- with-memory-sql
[f]
(let [db (new sqlite ":memory:" nil)
sql #js {:exec (fn [sql-str & args]
(let [stmt (.prepare db sql-str)]
(if (select-sql? sql-str)
(all-sql stmt args)
(do
(run-sql stmt args)
nil))))
:close (fn []
(.close db))}]
(try
(f sql)
(finally
(.close sql)))))
(defn- seeded-rng
[seed0]
(let [state (atom (bit-or (long seed0) 0))]
(fn []
(let [s (swap! state
(fn [x]
(let [x (bit-xor x (bit-shift-left x 13))
x (bit-xor x (bit-shift-right x 17))
x (bit-xor x (bit-shift-left x 5))]
(bit-or x 0))))]
(/ (double (unsigned-bit-shift-right s 0)) 4294967296.0)))))
(defn- rand-int*
[rng n]
(js/Math.floor (* (rng) n)))
(defn- pick-rand
[rng coll]
(when (seq coll)
(nth coll (rand-int* rng (count coll)))))
(defn- normal-block-uuids
[db]
(->> (d/datoms db :avet :block/uuid)
(map :e)
distinct
(keep (fn [eid]
(let [ent (d/entity db eid)]
(when (and (uuid? (:block/uuid ent))
(not (ldb/built-in? ent))
(nil? (:block/name ent))
(some? (:block/page ent)))
(:block/uuid ent)))))
vec))
(deftest t-meta-test
(let [sql (test-sql/make-sql)]
(storage/init-schema! sql)
(is (= 0 (storage/get-t sql)))
(storage/set-t! sql 1)
(is (= 1 (storage/get-t sql)))
(storage/set-t! sql 2)
(is (= 2 (storage/get-t sql)))))
(deftest tx-log-test
(let [sql (test-sql/make-sql)]
(storage/init-schema! sql)
(storage/append-tx! sql 1 "tx-1" 100 :save-block)
(storage/append-tx! sql 2 "tx-2" 200 :move-blocks)
(storage/append-tx! sql 3 "tx-3" 300 nil)
(let [result (storage/fetch-tx-since sql 1)]
(is (= [{:t 2 :tx "tx-2" :outliner-op :move-blocks}
{:t 3 :tx "tx-3" :outliner-op nil}]
result)))))
(deftest stale-checksum-no-op-transact-does-not-throw-test
(testing "a no-op tx should not throw and should keep incremental checksum state"
(with-memory-sql
(fn [sql]
(let [stale-checksum "f4b78e83776d45fb"]
(storage/init-schema! sql)
(storage/set-checksum! sql stale-checksum)
(let [conn (storage/open-conn sql)
result (try
(d/transact! conn
[]
{:outliner-op :rebase})
:ok
(catch :default e
e))]
(is (= :ok result))
(is (= stale-checksum
(storage/get-checksum sql)))))))))
(deftest initial-checksum-does-not-overwrite-existing-checksum-test
(testing "snapshot checksum initialization must not replace an existing incremental checksum"
(let [sql (test-sql/make-sql)
existing-checksum "aaaaaaaaaaaaaaaa"
snapshot-checksum "bbbbbbbbbbbbbbbb"]
(storage/init-schema! sql)
(storage/set-initial-checksum! sql existing-checksum)
(let [result (try
(storage/set-initial-checksum! sql snapshot-checksum)
:ok
(catch :default error
error))]
(is (not= :ok result))
(is (= existing-checksum (storage/get-checksum sql)))))))
(deftest initial-checksum-rejects-non-empty-tx-log-test
(testing "snapshot checksum initialization must not run after tx history already advanced"
(let [sql (test-sql/make-sql)
snapshot-checksum "bbbbbbbbbbbbbbbb"]
(storage/init-schema! sql)
(storage/append-tx! sql 1 "tx-1" 100 :save-block)
(storage/set-t! sql 1)
(let [result (try
(storage/set-initial-checksum! sql snapshot-checksum)
:ok
(catch :default error
error))]
(is (not= :ok result))
(is (nil? (storage/get-checksum sql)))))))
(deftest stale-checksum-transact-keeps-kvs-and-tx-log-consistent-test
(testing "stale checksum should not fail transact; kvs and tx_log/t should advance together"
(with-memory-sql
(fn [sql]
(storage/init-schema! sql)
(let [conn (storage/open-conn sql)
stale-checksum "ffffffffffffffff"
page-uuid (random-uuid)]
;; Use a stale checksum and ensure append path remains consistent.
(storage/set-checksum! sql stale-checksum)
(let [result (try
(d/transact! conn [{:block/uuid page-uuid
:block/name "repro-kvs-ahead-page"}])
:ok
(catch :default e
e))]
(is (= :ok result))
(is (= 1 (storage/get-t sql)))
(is (= 1 (count (storage/fetch-tx-since sql 0))))
(is (not= stale-checksum (storage/get-checksum sql)))
(let [restored-conn (storage/open-conn sql)]
(is (= page-uuid
(:block/uuid (d/entity @restored-conn [:block/uuid page-uuid])))))))))))
(deftest tx-log-append-failure-does-not-advance-checksum-test
(testing "server checksum and t should not advance when the tx log append fails"
(with-memory-sql
(fn [sql]
(storage/init-schema! sql)
(let [conn (storage/open-conn sql)
page-uuid (random-uuid)
failed-block-uuid (random-uuid)]
(d/transact! conn [{:block/uuid page-uuid
:block/name "tx-log-failure-repro"}])
(let [checksum-before (storage/get-checksum sql)
t-before (storage/get-t sql)
original-sql-exec common/sql-exec
result (with-redefs [common/sql-exec
(fn [sql sql-str & args]
(if (string/includes? sql-str "insert into tx_log")
(throw (js/Error. "tx log append failed"))
(apply original-sql-exec sql sql-str args)))]
(try
(d/transact! conn [{:block/uuid failed-block-uuid
:block/title "failed append"
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid page-uuid]}])
:ok
(catch :default error
error)))]
(is (not= :ok result))
(is (= t-before (storage/get-t sql)))
(is (= checksum-before (storage/get-checksum sql)))))))))
(deftest normalize-drop-can-hide-kvs-mutation-from-tx-log-test
(testing "if normalize drops tx payload, tx_log can miss persisted kvs state changes"
(with-memory-sql
(fn [sql]
(storage/init-schema! sql)
(let [conn (storage/open-conn sql)
page-uuid (random-uuid)]
(with-redefs [db-normalize/normalize-tx-data (fn [_db-after _db-before _tx-data]
[])]
(d/transact! conn [{:block/uuid page-uuid
:block/name "normalize-drop-repro"}]))
(is (= 1 (storage/get-t sql)))
(let [entries (storage/fetch-tx-since sql 0)]
(is (= 1 (count entries)))
(is (= []
(common/read-transit (:tx (first entries)))))
(let [restored-conn (storage/open-conn sql)]
(is (= page-uuid
(:block/uuid (d/entity @restored-conn [:block/uuid page-uuid])))))))))))
(deftest randomized-normal-block-retract-recreate-does-not-throw-checksum-mismatch-test
(testing "normal block retract/recreate patterns should not naturally trigger server checksum mismatch"
(with-memory-sql
(fn [sql]
(storage/init-schema! sql)
(let [conn (storage/open-conn sql)
rng (seeded-rng 424242)
page-uuid (random-uuid)
_ (d/transact! conn [{:block/uuid page-uuid
:block/name "repro-page"
:block/title "repro-page"}])
_ (d/transact! conn (mapv (fn [idx]
{:block/uuid (random-uuid)
:block/title (str "seed-" idx)
:block/page [:block/uuid page-uuid]
:block/parent [:block/uuid page-uuid]
:block/order (str "a" idx)})
(range 5)))
*mismatch (atom nil)]
(dotimes [step 500]
(when-not @*mismatch
(let [db @conn
blocks (normal-block-uuids db)
target (pick-rand rng blocks)
sibling (pick-rand rng (remove #(= % target) blocks))
add-fresh? (< (rng) 0.3)
tx-data (cond-> [[:db/retractEntity [:block/uuid target]]
[:db/add -1 :block/uuid target]
[:db/add -1 :block/title (str "rr-" step)]
[:db/add -1 :block/page [:block/uuid page-uuid]]
[:db/add -1 :block/parent [:block/uuid page-uuid]]
[:db/add -1 :block/order (str "z" (mod step 7))]]
sibling
(conj [:db/add [:block/uuid sibling] :block/title (str "sib-" step)])
add-fresh?
(into [[:db/add -2 :block/uuid (random-uuid)]
[:db/add -2 :block/title (str "fresh-" step)]
[:db/add -2 :block/page [:block/uuid page-uuid]]
[:db/add -2 :block/parent [:block/uuid page-uuid]]
[:db/add -2 :block/order (str "x" (mod step 9))]]))]
(try
(d/transact! conn tx-data)
(catch :default e
(let [message (or (ex-message e) (some-> e .-message) (str e))]
(if (string/includes? message "server checksum doesn't match")
(reset! *mismatch {:step step
:tx-data tx-data
:error message})
;; tx can be invalid due random -2 fresh fields; ignore non-checksum failures
nil)))))))
(is (nil? @*mismatch)
(str "found checksum mismatch repro: " (pr-str @*mismatch))))))))

View File

@@ -0,0 +1,36 @@
(ns logseq.db-sync.test-runner
(:require [cljs.test :as ct]
[logseq.db-sync.checksum-test]
[logseq.db-sync.common-test]
[logseq.db-sync.index-test]
[logseq.db-sync.malli-schema-test]
[logseq.db-sync.node-adapter-test]
[logseq.db-sync.node-config-test]
[logseq.db-sync.node-server-test]
[logseq.db-sync.normalize-test]
[logseq.db-sync.storage-test]
[logseq.db-sync.tx-sanitize-test]
[logseq.db-sync.worker-auth-test]
[logseq.db-sync.worker-dispatch-test]
[logseq.db-sync.worker-handler-assets-test]
[logseq.db-sync.worker-handler-index-test]
[logseq.db-sync.worker-handler-sync-test]
[logseq.db-sync.worker-handler-ws-test]
[shadow.test :as st]
[shadow.test.env :as env]))
(derive ::node ::ct/default)
(defmethod ct/report [::node :end-run-tests] [m]
(if (ct/successful? m)
(js/process.exit 0)
(js/process.exit 1)))
(defn ^:dev/after-load reset-test-data! []
(-> (env/get-test-data)
(env/reset-test-data!)))
(defn main [& _args]
(reset-test-data!)
(let [test-env (ct/empty-env ::node)]
(st/run-all-tests test-env nil)))

View File

@@ -0,0 +1,56 @@
(ns logseq.db-sync.test-sql
(:require [clojure.string :as string]))
(defn- js-key [k]
(cond
(keyword? k) (string/replace (name k) "-" "_")
(string? k) k
:else (str k)))
(defn- js-row [m]
(let [o (js-obj)]
(doseq [[k v] m]
(aset o (js-key k) v))
o))
(defn- js-rows [rows]
(into-array (map js-row rows)))
(defn make-sql []
(let [state (atom {:tx-log {}
:meta {}})]
#js {:exec (fn [sql & args]
(cond
(string/includes? sql "insert into tx_log")
(let [[t tx created-at outliner-op] args]
(swap! state update :tx-log assoc t {:t t
:tx tx
:created-at created-at
:outliner-op outliner-op})
nil)
(string/includes? sql "select t, tx, outliner_op from tx_log")
(let [since (first args)
rows (->> (:tx-log @state)
vals
(filter (fn [row] (> (:t row) since)))
(sort-by :t)
(map (fn [row] {:t (:t row)
:tx (:tx row)
:outliner-op (:outliner-op row)})))]
(js-rows rows))
(string/includes? sql "insert into sync_meta")
(let [[k v] args]
(swap! state update :meta assoc k v)
nil)
(string/includes? sql "select value from sync_meta")
(let [k (first args)
value (get-in @state [:meta k])]
(if (some? value)
(js-rows [{:value value}])
(js-rows [])))
:else
nil))}))

View File

@@ -0,0 +1,104 @@
(ns logseq.db-sync.tx-sanitize-test
(:require [cljs.test :refer [deftest is testing]]
[clojure.set :as set]
[datascript.core :as d]
[logseq.db-sync.tx-sanitize :as tx-sanitize]
[logseq.db.test.helper :as db-test]))
(def ^:private migration-deleted-attrs
#{:block/path-refs
:block/pre-block?
:logseq.property.embedding/hnsw-label
:logseq.property.embedding/hnsw-label-updated-at})
(def ^:private graph-backup-folder-ops
#{[:db/retractEntity :logseq.kv/graph-backup-folder]
[:db/add :logseq.kv/graph-backup-folder :logseq.kv/value "/tmp/backup"]})
(defn- tx-item-attrs
[item]
(cond
(map? item)
(set (keys item))
(and (vector? item) (<= 4 (count item)))
#{(nth item 2)}
:else
#{}))
(deftest sanitize-tx-drops-migration-deleted-attrs-test
(testing "remote txs from older clients should not reintroduce attrs deleted by client migrations"
(let [conn (db-test/create-conn)
block-uuid #uuid "11111111-1111-1111-1111-111111111111"
tx-data [[:db/add [:block/uuid block-uuid] :block/title "remote title"]
[:db/add [:block/uuid block-uuid] :block/path-refs #uuid "22222222-2222-2222-2222-222222222222"]
[:db/retract [:block/uuid block-uuid] :block/pre-block? true]
[:db/add [:block/uuid block-uuid] :logseq.property.embedding/hnsw-label "label"]
[:db/retract [:block/uuid block-uuid] :logseq.property.embedding/hnsw-label-updated-at 123]]
sanitized (tx-sanitize/sanitize-tx @conn tx-data)
sanitized-attrs (set (mapcat tx-item-attrs sanitized))]
(is (empty? (set/intersection migration-deleted-attrs sanitized-attrs)))
(is (some #(= [:db/add [:block/uuid block-uuid] :block/title "remote title"] %) sanitized)))))
(deftest sanitize-tx-drops-ignored-kv-entity-ops-test
(testing "remote txs should not apply KV entities that are excluded from sync"
(let [block-uuid #uuid "11111111-1111-1111-1111-111111111111"
conn (db-test/create-conn)
tx-data (into [[:db/add [:block/uuid block-uuid] :block/title "remote title"]]
graph-backup-folder-ops)
sanitized (tx-sanitize/sanitize-tx @conn tx-data)]
(is (empty? (set/intersection graph-backup-folder-ops (set sanitized))))
(is (some #(= [:db/add [:block/uuid block-uuid] :block/title "remote title"] %) sanitized)))))
(deftest sanitize-tx-drops-same-tx-ignored-kv-tempid-ops-test
(testing "remote txs should drop all ops for tempids identified as ignored KV entities"
(let [block-uuid #uuid "11111111-1111-1111-1111-111111111111"
conn (db-test/create-conn)
ignored-ops #{[:db/add "kv-temp" :db/ident :logseq.kv/graph-backup-folder]
[:db/add "kv-temp" :logseq.kv/value "/tmp/backup"]
[:db/retractEntity "kv-temp"]}
tx-data (into [[:db/add [:block/uuid block-uuid] :block/title "remote title"]]
ignored-ops)
sanitized (tx-sanitize/sanitize-tx @conn tx-data)]
(is (empty? (set/intersection ignored-ops (set sanitized))))
(is (some #(= [:db/add [:block/uuid block-uuid] :block/title "remote title"] %) sanitized)))))
(deftest sanitize-tx-drops-same-tx-ignored-kv-map-ops-test
(testing "remote txs should drop map-form ignored KV entities and following tempid ops"
(let [block-uuid #uuid "11111111-1111-1111-1111-111111111111"
conn (db-test/create-conn)
ignored-map {:db/id -1
:db/ident :logseq.kv/graph-backup-folder
:logseq.kv/value "/tmp/backup"}
ignored-ops #{ignored-map
[:db/add -1 :logseq.kv/value "/tmp/backup-2"]
[:db/retractEntity -1]}
tx-data (into [[:db/add [:block/uuid block-uuid] :block/title "remote title"]]
ignored-ops)
sanitized (tx-sanitize/sanitize-tx @conn tx-data)]
(is (empty? (set/intersection ignored-ops (set sanitized))))
(is (some #(= [:db/add [:block/uuid block-uuid] :block/title "remote title"] %) sanitized)))))
(deftest sanitize-delete-blocks-retracts-property-value-children-test
(testing "delete-blocks adds retracts for generated property value children"
(let [conn (db-test/create-conn-with-blocks
{:properties {:user.property/cli-http-prop {:logseq.property/type :default}}
:pages-and-blocks
[{:page {:block/title "Page"}
:blocks [{:block/title "Parent"
:build/properties
{:user.property/cli-http-prop
{:build/property-value :block
:block/title "Property value"}}}]}]})
parent (db-test/find-block-by-content @conn "Parent")
property-value (db-test/find-block-by-content @conn "Property value")
tx-data [[:db/retractEntity [:block/uuid (:block/uuid parent)]]]
sanitized (tx-sanitize/sanitize-tx @conn
tx-data
{:drop-missing-retract-ops? true
:drop-ops-targeting-retracted-entities? true
:retract-touched-descendants? true})]
(is (= (:db/id property-value)
(:db/id (:user.property/cli-http-prop (d/entity @conn (:db/id parent))))))
(is (some #(= [:db/retractEntity (:db/id property-value)] %) sanitized)))))

View File

@@ -0,0 +1,107 @@
(ns logseq.db-sync.worker-auth-test
(:require [cljs.test :refer [async deftest is]]
[logseq.common.authorization :as authorization]
[logseq.db-sync.worker.auth :as auth]
[promesa.core :as p]))
(deftest cognito-client-id-allowlist-test
(let [env #js {"COGNITO_CLIENT_ID" "web-client"
"COGNITO_CLIENT_IDS" "chatgpt-client, admin-client"}]
(is (authorization/client-id-allowed? env "web-client"))
(is (authorization/client-id-allowed? env "chatgpt-client"))
(is (authorization/client-id-allowed? env "admin-client"))
(is (not (authorization/client-id-allowed? env "unknown-client")))))
(deftest cognito-client-id-allowlist-fails-closed-test
(is (not (authorization/client-id-allowed? #js {} nil)))
(is (not (authorization/client-id-allowed? #js {} "")))
(is (not (authorization/client-id-allowed? #js {"COGNITO_CLIENT_ID" ""} "")))
(is (not (authorization/client-id-allowed? #js {"COGNITO_CLIENT_IDS" " , "} "chatgpt-client")))
(is (not (authorization/client-id-allowed? #js {"COGNITO_CLIENT_ID" "web-client"} nil))))
(deftest auth-claims-uses-jwt-verification-test
(async done
(let [request (js/Request. "http://localhost/graphs"
#js {:headers #js {"authorization" "Bearer dev-token"}})]
(-> (p/with-redefs [authorization/verify-jwt
(fn [token _env]
(js/Promise.resolve #js {"sub" (str "jwt:" token)}))]
(p/let [claims (auth/auth-claims request #js {})]
(is (= "jwt:dev-token" (aget claims "sub")))))
(p/then (fn [] (done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest auth-claims-without-token-returns-nil-test
(async done
(let [request (js/Request. "http://localhost/graphs")]
(-> (p/let [claims (auth/auth-claims request #js {})]
(is (nil? claims)))
(p/then (fn [] (done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest auth-claims-expired-token-returns-nil-test
(async done
(let [request (js/Request. "http://localhost/graphs"
#js {:headers #js {"authorization" "Bearer expired-token"}})]
(-> (p/with-redefs [authorization/verify-jwt
(fn [_token _env]
(p/rejected (ex-info "exp" {})))]
(p/let [claims (auth/auth-claims request #js {})]
(is (nil? claims))))
(p/then (fn [] (done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest auth-claims-jwks-error-propagates-test
(async done
(let [request (js/Request. "http://localhost/graphs"
#js {:headers #js {"authorization" "Bearer broken-token"}})]
(-> (p/with-redefs [authorization/verify-jwt
(fn [_token _env]
(p/rejected (ex-info "jwks" {})))]
(auth/auth-claims request #js {}))
(p/then (fn [_]
(is false "expected rejection when jwks fetch fails")
(done)))
(p/catch (fn [error]
(is (= "jwks" (ex-message error)))
(done)))))))
(deftest auth-claims-jwks-error-falls-back-to-unsafe-claims-when-enabled-test
(async done
(let [token "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1MSJ9.signature"
request (js/Request. "http://localhost/graphs"
#js {:headers #js {"authorization" (str "Bearer " token)}})
env #js {"DB_SYNC_ALLOW_UNVERIFIED_JWT_CLAIMS" "true"}]
(-> (p/with-redefs [authorization/verify-jwt
(fn [_token _env]
(p/rejected (ex-info "jwks" {})))]
(p/let [claims (auth/auth-claims request env)]
(is (= "u1" (aget claims "sub")))))
(p/then (fn [] (done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest auth-claims-expired-jwt-short-circuits-verification-test
(async done
(let [expired-token "eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjEsInN1YiI6InUxIn0.signature"
request (js/Request. "http://localhost/graphs"
#js {:headers #js {"authorization" (str "Bearer " expired-token)}})
verify-called? (atom false)]
(-> (p/with-redefs [authorization/verify-jwt
(fn [_token _env]
(reset! verify-called? true)
(p/rejected (ex-info "should-not-be-called" {})))]
(p/let [claims (auth/auth-claims request #js {})]
(is (nil? claims))
(is (false? @verify-called?))))
(p/then (fn [] (done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))

View File

@@ -0,0 +1,392 @@
(ns logseq.db-sync.worker-dispatch-test
(:require [cljs.test :refer [async deftest is]]
[clojure.string :as string]
[logseq.db-sync.common :as common]
[logseq.db-sync.index :as index]
[logseq.db-sync.worker.auth :as auth]
[logseq.db-sync.worker.dispatch :as dispatch]
[logseq.db-sync.worker.handler.assets :as assets-handler]
[logseq.db-sync.worker.handler.index :as index-handler]
[logseq.db-sync.worker.http :as http]
[promesa.core :as p]))
(defn- ok-json-response []
(js/Response. (js/JSON.stringify #js {:ok true})
#js {:status 200
:headers #js {"content-type" "application/json"}}))
(defn- make-do-namespace []
#js {:idFromName (fn [_graph-id] "do-id")
:get (fn [_do-id]
#js {:fetch (fn [_request]
(js/Promise.resolve (ok-json-response)))})})
(declare capturing-do-namespace json-body)
(deftest admin-token-bypasses-graph-access-check-for-sync-route-test
(async done
(let [access-check-calls (atom 0)
request (js/Request. "http://localhost/sync/graph-1/snapshot/download"
#js {:method "GET"
:headers #js {"x-db-sync-admin-token" "test-admin-token"}})
env #js {"DB_SYNC_ADMIN_TOKEN" "test-admin-token"
"LOGSEQ_SYNC_DO" (make-do-namespace)}]
(-> (p/with-redefs [index-handler/graph-access-response (fn [_request _env _graph-id]
(swap! access-check-calls inc)
(p/resolved (http/unauthorized)))]
(p/let [resp (dispatch/handle-worker-fetch request env)
text (.text resp)
body (js->clj (js/JSON.parse text) :keywordize-keys true)]
(is (= 200 (.-status resp)))
(is (= true (:ok body)))
(is (= 0 @access-check-calls))))
(p/then (fn [] (done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest public-sync-route-rejects-semantic-tail-without-touching-durable-object-test
(async done
(let [forwarded (atom [])
request (js/Request. "http://localhost/sync/graph-1/semantic/pages"
#js {:method "GET"})
env #js {"LOGSEQ_SYNC_DO" (capturing-do-namespace forwarded)}]
(-> (p/with-redefs [index-handler/graph-access-response
(fn [_request _env _graph-id]
(p/resolved (ok-json-response)))]
(p/let [response (dispatch/handle-worker-fetch request env)
body (json-body response)]
(is (= 404 (.-status response)))
(is (= "not found" (:error body)))
(is (empty? @forwarded))))
(p/then (fn [] (done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest public-sync-route-still-forwards-legacy-snapshot-request-test
(async done
(let [forwarded (atom [])
request (js/Request. "http://localhost/sync/graph-1/snapshot/download"
#js {:method "GET"})
env #js {"LOGSEQ_SYNC_DO" (capturing-do-namespace forwarded)}]
(-> (p/with-redefs [index-handler/graph-access-response
(fn [_request _env _graph-id]
(p/resolved (ok-json-response)))]
(p/let [response (dispatch/handle-worker-fetch request env)]
(is (= 200 (.-status response)))
(is (= 1 (count @forwarded)))
(is (= "/snapshot/download"
(.-pathname (js/URL. (.-url (first @forwarded))))))))
(p/then (fn [] (done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(defn- json-body [response]
(p/let [text (.text response)]
(js->clj (js/JSON.parse text) :keywordize-keys true)))
(defn- semantic-request
([path scope]
(semantic-request path scope "GET"))
([path scope method]
{:request (js/Request. (str "http://localhost" path)
#js {:method method
:headers #js {"authorization" "Bearer semantic-token"}})
:claims #js {"sub" "user-1" "scope" scope}}))
(defn- graph-row [e2ee?]
#js {:graph_e2ee (if e2ee? 1 0)})
(defn- rate-limiter [success? calls]
#js {:limit (fn [opts]
(swap! calls conj (js->clj opts :keywordize-keys true))
(p/resolved #js {:success success?}))})
(defn- capturing-do-namespace [requests]
#js {:idFromName (fn [graph-id] graph-id)
:get (fn [_]
#js {:fetch (fn [request]
(swap! requests conj request)
(p/resolved (ok-json-response)))})})
(deftest openapi-document-describes-semantic-api-and-oauth-scopes-test
(async done
(-> (p/let [response (dispatch/handle-worker-fetch
(js/Request. "http://localhost/openapi.json")
#js {})
body (json-body response)]
(is (= 200 (.-status response)))
(is (= "3.1.0" (:openapi body)))
(is (= ["logseq/read"]
(get-in body [:paths (keyword "/api/v1/graphs") :get :security 0 :oauth])))
(is (= ["logseq/read"]
(get-in body [:paths (keyword "/api/v1/graphs/{graph-id}/pages") :get :security 0 :oauth])))
(is (= ["logseq/write"]
(get-in body [:paths (keyword "/api/v1/graphs/{graph-id}/blocks/{block-id}") :patch :security 0 :oauth])))
(is (= ["logseq/write"]
(get-in body [:paths (keyword "/api/v1/graphs/{graph-id}/pages/{page-id}") :delete :security 0 :oauth])))
(doseq [path ["/api/v1/graphs/{graph-id}/pages/{page-id}"
"/api/v1/graphs/{graph-id}/tags/{tag-id}"
"/api/v1/graphs/{graph-id}/properties/{property-id}"]]
(is (= #{:get :patch :delete} (set (keys (get-in body [:paths (keyword path)]))))))
(is (= ["logseq/write"]
(get-in body [:paths (keyword "/api/v1/graphs/{graph-id}/blocks/{block-id}/properties/{property-id}")
:delete :security 0 :oauth])))
(let [description (->> (get-in body [:paths (keyword "/api/v1/graphs/{graph-id}/blocks/{block-id}/properties/{property-id}")
:put :parameters])
(filter #(= "property-id" (:name %))) first :description)]
(is (and (string? description)
(string/includes? description "exact title"))))
(is (= ["logseq/write"]
(get-in body [:paths (keyword "/api/v1/graphs/{graph-id}/block-properties/batch-delete")
:post :security 0 :oauth])))
(is (= ["block-ids" "target-id" "position"]
(get-in body [:paths (keyword "/api/v1/graphs/{graph-id}/block-moves")
:post :requestBody :content :application/json :schema :required])))
(is (= "array"
(get-in body [:paths (keyword "/api/v1/graphs/{graph-id}/block-moves")
:post :requestBody :content :application/json :schema
:properties :block-ids :type])))
(is (= "#/components/schemas/BlockResponse"
(get-in body [:paths (keyword "/api/v1/graphs/{graph-id}/pages/{page-id}/blocks")
:get :responses :200 :content :application/json :schema
:properties :blocks :items :$ref])))
(is (= "#/components/schemas/BlockResponse"
(get-in body [:components :schemas :BlockResponse :properties :children :items :$ref])))
(is (= "listTasks"
(get-in body [:paths (keyword "/api/v1/graphs/{graph-id}/tasks") :get :operationId])))
(is (= "createTask"
(get-in body [:paths (keyword "/api/v1/graphs/{graph-id}/tasks") :post :operationId])))
(is (= ["todo" "doing" "in-review" "done" "canceled" "backlog"]
(get-in body [:components :schemas :TaskStatusSelector :anyOf 0 :enum])))
(is (= "#/components/schemas/EntitySelector"
(get-in body [:components :schemas :TaskStatusSelector :anyOf 1 :$ref])))
(is (= "#/components/schemas/PropertyChoice"
(get-in body [:components :schemas :TaskResponse :allOf 1 :properties :status :$ref])))
(is (= "#/components/schemas/TaskResponse"
(get-in body [:paths (keyword "/api/v1/graphs/{graph-id}/tasks")
:get :responses :200 :content :application/json :schema
:properties :tasks :items :$ref])))
(is (= "#/components/schemas/TaskResponse"
(get-in body [:paths (keyword "/api/v1/graphs/{graph-id}/tasks")
:post :responses :201 :content :application/json :schema :$ref])))
(is (= "#/components/schemas/PropertyResponse"
(get-in body [:paths (keyword "/api/v1/graphs/{graph-id}/properties/{property-id}")
:get :responses :200 :content :application/json :schema :$ref])))
(is (= ["default" "number" "date" "datetime" "checkbox" "url" "node" "asset"
"map" "json" "string"]
(get-in body [:components :schemas :PropertyType :enum])))
(is (= "#/components/schemas/PropertyType"
(get-in body [:paths (keyword "/api/v1/graphs/{graph-id}/properties")
:post :requestBody :content :application/json :schema
:properties :type :$ref])))
(doseq [[path response-key schema]
[["/api/v1/graphs/{graph-id}/pages" :pages "PageResponse"]
["/api/v1/graphs/{graph-id}/tags" :tags "TagResponse"]
["/api/v1/graphs/{graph-id}/properties" :properties "PropertyResponse"]
["/api/v1/graphs/{graph-id}/search" :results "SearchResultResponse"]]]
(is (= (str "#/components/schemas/" schema)
(get-in body [:paths (keyword path) :get :responses :200
:content :application/json :schema :properties
response-key :items :$ref]))))
(is (= "#/components/schemas/PropertyChoice"
(get-in body [:components :schemas :PropertyResponse :properties
:choices :items :$ref])))
(is (= "#/components/schemas/PropertyValue"
(get-in body [:paths (keyword "/api/v1/graphs/{graph-id}/blocks/{block-id}/properties/{property-id}")
:put :requestBody :content :application/json :schema
:properties :value :$ref])))
(is (= {:type "boolean" :default false}
(select-keys
(get-in body [:paths (keyword "/api/v1/graphs/{graph-id}/block-properties/batch-set")
:post :requestBody :content :application/json :schema
:properties :isResetExistingValues])
[:type :default])))
(is (string/includes?
(get-in body [:components :schemas :PropertyValue :description])
"UUID, ident, or title"))
(is (string/includes?
(get-in body [:components :schemas :PropertyValue :description])
"TODO"))
(doseq [path ["/api/v1/graphs/{graph-id}/pages"
"/api/v1/graphs/{graph-id}/tasks"
"/api/v1/graphs/{graph-id}/tags"
"/api/v1/graphs/{graph-id}/tags/{tag-id}/objects"
"/api/v1/graphs/{graph-id}/properties"
"/api/v1/graphs/{graph-id}/assets"
"/api/v1/graphs/{graph-id}/search"]]
(is (= #{"created-after" "updated-after"}
(->> (get-in body [:paths (keyword path) :get :parameters])
(map :name)
(filter #{"created-after" "updated-after"})
set))
path))
(is (= "listAssets"
(get-in body [:paths (keyword "/api/v1/graphs/{graph-id}/assets") :get :operationId])))
(is (= "createAsset"
(get-in body [:paths (keyword "/api/v1/graphs/{graph-id}/assets") :post :operationId])))
(is (string/includes?
(get-in body [:paths (keyword "/api/v1/graphs/{graph-id}/assets/{asset-block-id}")
:get :description])
"display image"))
(is (= "binary"
(get-in body [:paths (keyword "/api/v1/graphs/{graph-id}/assets") :post
:requestBody :content :application/octet-stream :schema :format])))
(is (= #{"file-name" "size" "title" "page-id" "checksum" "encoding"}
(->> (get-in body [:paths (keyword "/api/v1/graphs/{graph-id}/assets")
:post :parameters])
(map :name)
(filter #{"file-name" "size" "title" "page-id" "checksum" "encoding"})
set)))
(is (= "#/components/schemas/AssetResponse"
(get-in body [:paths (keyword "/api/v1/graphs/{graph-id}/assets") :get
:responses :200 :content :application/json :schema
:properties :assets :items :$ref])))
(is (= "#/components/schemas/AssetResponse"
(get-in body [:paths (keyword "/api/v1/graphs/{graph-id}/assets") :post
:responses :201 :content :application/json :schema :$ref])))
(doseq [[path method] [["/api/v1/graphs/{graph-id}/properties" :post]
["/api/v1/graphs/{graph-id}/properties/{property-id}" :patch]
["/api/v1/graphs/{graph-id}/blocks/{block-id}/properties/{property-id}" :put]
["/api/v1/graphs/{graph-id}/block-properties/batch-set" :post]]]
(let [description (get-in body [:paths (keyword path) method :description])]
(is (string/includes? description "DB graph"))
(is (string/includes? description "typed"))
(is (string/includes? description "key:: value"))))
(doseq [[_ path-operations] (:paths body)
[_ operation] path-operations]
(is (seq (:summary operation)))
(is (seq (:description operation)))))
(p/then (fn [] (done)))
(p/catch (fn [error]
(is false (str error))
(done))))))
(deftest semantic-api-lists-non-e2ee-graphs-with-pagination-test
(async done
(let [{:keys [request claims]} (semantic-request "/api/v1/graphs?name=test-mcp&limit=1" "logseq/read")
calls (atom [])
limit-calls (atom [])
env #js {"DB" #js {}
"SEMANTIC_READ_RATE_LIMITER" (rate-limiter true limit-calls)}]
(-> (p/with-redefs [auth/auth-claims (fn [_ _] (p/resolved claims))
index/<semantic-graphs-list
(fn [_db user-id opts]
(swap! calls conj [user-id opts])
(p/resolved {:graphs [{:graph-id "graph-1" :graph-name "test-mcp"}]
:next-cursor "cursor-1"}))]
(p/let [response (dispatch/handle-worker-fetch request env)
body (json-body response)]
(is (= 200 (.-status response)))
(is (= [{:graph-id "graph-1" :graph-name "test-mcp"}] (:graphs body)))
(is (= "cursor-1" (:next-cursor body)))
(is (= [["user-1" {:name "test-mcp" :limit 1 :cursor nil}]] @calls))
(is (= 1 (count @limit-calls)))))
(p/then (fn [] (done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest semantic-api-rejects-missing-operation-scope-test
(async done
(let [{:keys [request claims]} (semantic-request "/api/v1/graphs/graph-1/pages" "logseq/write")]
(-> (p/with-redefs [auth/auth-claims (fn [_ _] (p/resolved claims))]
(p/let [response (dispatch/handle-worker-fetch request #js {})
body (json-body response)]
(is (= 403 (.-status response)))
(is (= "insufficient scope" (:error body)))))
(p/then (fn [] (done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest semantic-api-rejects-e2ee-graphs-before-durable-object-test
(async done
(let [{:keys [request claims]} (semantic-request "/api/v1/graphs/graph-1/pages" "logseq/read")
forwarded (atom [])
env #js {"DB" #js {}
"LOGSEQ_SYNC_DO" (capturing-do-namespace forwarded)
"SEMANTIC_READ_RATE_LIMITER" (rate-limiter true (atom []))}]
(-> (p/with-redefs [auth/auth-claims (fn [_ _] (p/resolved claims))
index-handler/graph-access-response (fn [_ _ _] (p/resolved (ok-json-response)))
common/<d1-all (fn [& _] (p/resolved #js {:results #js [(graph-row true)]}))]
(p/let [response (dispatch/handle-worker-fetch request env)
body (json-body response)]
(is (= 409 (.-status response)))
(is (= "semantic-api-unavailable-for-e2ee" (:error body)))
(is (empty? @forwarded))))
(p/then (fn [] (done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest semantic-api-rate-limit-rejection-does-not-call-durable-object-test
(async done
(let [{:keys [request claims]} (semantic-request "/api/v1/graphs/graph-1/pages" "logseq/read")
forwarded (atom [])
limit-calls (atom [])
env #js {"DB" #js {}
"LOGSEQ_SYNC_DO" (capturing-do-namespace forwarded)
"SEMANTIC_READ_RATE_LIMITER" (rate-limiter false limit-calls)}]
(-> (p/with-redefs [auth/auth-claims (fn [_ _] (p/resolved claims))
index-handler/graph-access-response (fn [_ _ _] (p/resolved (ok-json-response)))
common/<d1-all (fn [& _] (p/resolved #js {:results #js [(graph-row false)]}))]
(p/let [response (dispatch/handle-worker-fetch request env)]
(is (= 429 (.-status response)))
(is (= "60" (.get (.-headers response) "retry-after")))
(is (= 1 (count @limit-calls)))
(is (empty? @forwarded))))
(p/then (fn [] (done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest semantic-api-forwards-authorized-non-e2ee-request-test
(async done
(let [{:keys [request claims]} (semantic-request "/api/v1/graphs/graph-1/pages?limit=10" "logseq/read")
forwarded (atom [])
limit-calls (atom [])
env #js {"DB" #js {}
"LOGSEQ_SYNC_DO" (capturing-do-namespace forwarded)
"SEMANTIC_READ_RATE_LIMITER" (rate-limiter true limit-calls)}]
(-> (p/with-redefs [auth/auth-claims (fn [_ _] (p/resolved claims))
index-handler/graph-access-response (fn [_ _ _] (p/resolved (ok-json-response)))
common/<d1-all (fn [& _] (p/resolved #js {:results #js [(graph-row false)]}))]
(p/let [response (dispatch/handle-worker-fetch request env)
forwarded-request (first @forwarded)
forwarded-url (some-> forwarded-request .-url js/URL.)]
(is (= 200 (.-status response)))
(is (= 1 (count @forwarded)))
(when forwarded-url
(is (= "/semantic/pages" (.-pathname forwarded-url)))
(is (= "10" (.get (.-searchParams forwarded-url) "limit")))
(is (= "graph-1" (.get (.-searchParams forwarded-url) "graph-id"))))))
(p/then (fn [] (done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest admin-token-bypasses-graph-access-check-for-assets-route-test
(async done
(let [access-check-calls (atom 0)
request (js/Request. "http://localhost/assets/graph-1/snapshot-1.snapshot"
#js {:method "GET"
:headers #js {"x-db-sync-admin-token" "test-admin-token"}})
env #js {"DB_SYNC_ADMIN_TOKEN" "test-admin-token"}]
(-> (p/with-redefs [index-handler/graph-access-response (fn [_request _env _graph-id]
(swap! access-check-calls inc)
(p/resolved (http/unauthorized)))
assets-handler/handle (fn [_request _env]
(ok-json-response))]
(p/let [resp (dispatch/handle-worker-fetch request env)
text (.text resp)
body (js->clj (js/JSON.parse text) :keywordize-keys true)]
(is (= 200 (.-status resp)))
(is (= true (:ok body)))
(is (= 0 @access-check-calls))))
(p/then (fn [] (done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))

View File

@@ -0,0 +1,144 @@
(ns logseq.db-sync.worker-handler-assets-test
(:require [cljs.test :refer [async deftest is]]
[logseq.db-sync.index :as index]
[logseq.db-sync.worker.handler.assets :as assets]
[promesa.core :as p]))
(defn- bytes->stream
[^js payload]
(js/ReadableStream.
#js {:start (fn [controller]
(.enqueue controller payload)
(.close controller))}))
(defn- put-request-with-size
[size]
#js {:url "http://localhost/assets/graph-1/asset-1.bin"
:method "PUT"
:headers #js {:get (constantly nil)}
:arrayBuffer (fn [] (p/resolved #js {:byteLength size}))})
(deftest encrypted-assets-allow-upload-at-larger-limit-test
(async done
(let [request (put-request-with-size (* 200 1024 1024))
put-calls (atom 0)
env #js {:DB #js {}
:LOGSEQ_SYNC_ASSETS
#js {:put (fn [& _]
(swap! put-calls inc)
(p/resolved nil))}}]
(-> (p/with-redefs [index/<graph-e2ee? (fn [_ _] (p/resolved true))]
(p/let [resp (assets/handle request env)]
(is (= 200 (.-status resp)))
(is (= 1 @put-calls))))
(p/then (fn [] (done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest unencrypted-assets-keep-existing-upload-limit-test
(async done
(let [request (put-request-with-size (* 150 1024 1024))
put-calls (atom 0)
env #js {:DB #js {}
:LOGSEQ_SYNC_ASSETS
#js {:put (fn [& _]
(swap! put-calls inc)
(p/resolved nil))}}]
(-> (p/with-redefs [index/<graph-e2ee? (fn [_ _] (p/resolved false))]
(p/let [resp (assets/handle request env)]
(is (= 413 (.-status resp)))
(is (zero? @put-calls))))
(p/then (fn [] (done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest encrypted-assets-still-reject-uploads-above-larger-limit-test
(async done
(let [request (put-request-with-size (inc (* 200 1024 1024)))
put-calls (atom 0)
env #js {:DB #js {}
:LOGSEQ_SYNC_ASSETS
#js {:put (fn [& _]
(swap! put-calls inc)
(p/resolved nil))}}]
(-> (p/with-redefs [index/<graph-e2ee? (fn [_ _] (p/resolved true))]
(p/let [resp (assets/handle request env)]
(is (= 413 (.-status resp)))
(is (zero? @put-calls))))
(p/then (fn [] (done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest assets-get-includes-content-length-header-test
(async done
(let [payload (js/Uint8Array. #js [1 2 3 4])
request (js/Request. "http://localhost/assets/graph-1/snapshot-1.snapshot"
#js {:method "GET"})
env #js {:LOGSEQ_SYNC_ASSETS
#js {:get (fn [_key]
(js/Promise.resolve
#js {:body payload
:size 4
:httpMetadata #js {:contentType "application/octet-stream"}}))}}]
(-> (p/let [resp (assets/handle request env)]
(is (= 200 (.-status resp)))
(is (= "4" (.get (.-headers resp) "content-length")))
(is (= "4" (.get (.-headers resp) "x-asset-size"))))
(p/then (fn []
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest assets-get-includes-content-length-with-stream-body-without-fixed-length-stream-test
(async done
(let [payload (js/Uint8Array. #js [1 2 3 4])
request (js/Request. "http://localhost/assets/graph-1/snapshot-1.snapshot"
#js {:method "GET"})
env #js {:LOGSEQ_SYNC_ASSETS
#js {:get (fn [_key]
(js/Promise.resolve
#js {:body (bytes->stream payload)
:size 4
:httpMetadata #js {:contentType "application/octet-stream"}}))}}
original-fixed-length-stream (.-FixedLengthStream js/globalThis)
restore! #(aset js/globalThis "FixedLengthStream" original-fixed-length-stream)]
(aset js/globalThis "FixedLengthStream" js/undefined)
(-> (p/let [resp (assets/handle request env)
buf (.arrayBuffer resp)]
(is (= 200 (.-status resp)))
(is (= "4" (.get (.-headers resp) "content-length")))
(is (= "4" (.get (.-headers resp) "x-asset-size")))
(is (= 4 (.-byteLength buf))))
(p/then (fn []
(restore!)
(done)))
(p/catch (fn [error]
(restore!)
(is false (str error))
(done)))))))
(deftest assets-get-serves-image-inline-with-extension-content-type-test
(async done
(let [payload (js/Uint8Array. #js [1 2 3 4])
request (js/Request. "http://localhost/assets/graph-1/image-id.png"
#js {:method "GET"})
env #js {:LOGSEQ_SYNC_ASSETS
#js {:get (fn [_key]
(p/resolved
#js {:body payload
:size 4
;; Existing Code Mode uploads stored the transfer MIME type.
:httpMetadata #js {:contentType "application/json"}}))}}]
(-> (p/let [resp (assets/handle request env)]
(is (= 200 (.-status resp)))
(is (= "image/png" (.get (.-headers resp) "content-type")))
(is (= "inline; filename=\"image-id.png\""
(.get (.-headers resp) "content-disposition"))))
(p/then (fn [] (done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))

View File

@@ -0,0 +1,260 @@
(ns logseq.db-sync.worker-handler-index-test
(:require [cljs.test :refer [async deftest is]]
[clojure.string :as string]
[logseq.db-sync.common :as common]
[logseq.db-sync.index :as index]
[logseq.db-sync.worker.auth :as auth]
[logseq.db-sync.worker.handler.index :as index-handler]
[promesa.core :as p]))
(deftest graph-access-response-with-timing-caches-result-test
(async done
(let [request (js/Request. "http://localhost/sync/graph-1"
#js {:headers #js {"authorization" "Bearer token-cache-hit"}})
env #js {"DB" #js {}}
auth-count (atom 0)
query-count (atom 0)]
(-> (p/with-redefs [auth/auth-claims (fn [_request _env]
(swap! auth-count inc)
(p/resolved #js {"sub" "user-1"}))
index/<user-has-access-to-graph? (fn [_db _graph-id _user-id]
(swap! query-count inc)
(p/resolved true))]
(p/let [first-result (index-handler/graph-access-response-with-timing request env "graph-1")
second-result (index-handler/graph-access-response-with-timing request env "graph-1")]
(is (= 200 (.-status (:response first-result))))
(is (= 200 (.-status (:response second-result))))
(is (= 1 @auth-count))
(is (= 1 @query-count))
(is (false? (get-in first-result [:timing :cache-hit?])))
(is (true? (get-in second-result [:timing :cache-hit?])))))
(p/then (fn []
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest graph-access-response-with-timing-does-not-upsert-user-test
(async done
(let [request (js/Request. "http://localhost/sync/graph-2"
#js {:headers #js {"authorization" "Bearer token-no-upsert"}})
env #js {"DB" #js {}}]
(-> (p/with-redefs [auth/auth-claims (fn [_request _env]
(p/resolved #js {"sub" "user-2"}))
index/<user-upsert! (fn [& _]
(throw (ex-info "should-not-upsert" {})))
index/<user-has-access-to-graph? (fn [_db _graph-id _user-id]
(p/resolved true))]
(p/let [result (index-handler/graph-access-response-with-timing request env "graph-2")]
(is (= 200 (.-status (:response result))))
(is (= true (get-in result [:timing :access-ok?])))))
(p/then (fn []
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest graphs-create-defaults-ready-for-use-true-test
(async done
(let [request (js/Request. "http://localhost/graphs" #js {:method "POST"})
url (js/URL. (.-url request))
d1-runs (atom [])]
(-> (p/with-redefs [common/read-json (fn [_]
(p/resolved #js {"graph-name" "Graph 1"
"schema-version" "65"}))
index/<user-rsa-key-pair (fn [_db _user-id]
(p/resolved {:public-key "pk"
:encrypted-private-key "enc"}))
common/<d1-all (fn [& _]
(p/resolved #js {:results #js []}))
common/get-sql-rows (fn [result]
(aget result "results"))
common/<d1-run (fn [_db sql & args]
(swap! d1-runs conj {:sql sql
:args args})
(p/resolved {:ok true}))]
(p/let [resp (index-handler/handle {:db :db
:env #js {}
:request request
:url url
:claims #js {"sub" "user-1"}
:route {:handler :graphs/create
:path-params {}}})
text (.text resp)
body (js->clj (js/JSON.parse text) :keywordize-keys true)
graph-insert (first @d1-runs)]
(is (= 200 (.-status resp)))
(is (string/includes? (:sql graph-insert) "graph_ready_for_use"))
(is (= 1 (nth (:args graph-insert) 5)))
(is (= true (:graph-ready-for-use? body)))))
(p/then (fn []
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest graphs-list-includes-user-rsa-keys-exists-flag-true-test
(async done
(let [request (js/Request. "http://localhost/graphs" #js {:method "GET"})
url (js/URL. (.-url request))]
(-> (p/with-redefs [index/<index-list (fn [_db _user-id]
(p/resolved []))
index/<user-rsa-key-pair (fn [_db _user-id]
(p/resolved {:public-key "pk"
:encrypted-private-key "enc"}))]
(p/let [resp (index-handler/handle {:db :db
:env #js {}
:request request
:url url
:claims #js {"sub" "user-1"}
:route {:handler :graphs/list
:path-params {}}})
text (.text resp)
body (js->clj (js/JSON.parse text) :keywordize-keys true)]
(is (= 200 (.-status resp)))
(is (= [] (:graphs body)))
(is (= true (:user-rsa-keys-exists? body)))))
(p/then (fn []
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest graphs-list-includes-user-rsa-keys-exists-flag-false-test
(async done
(let [request (js/Request. "http://localhost/graphs" #js {:method "GET"})
url (js/URL. (.-url request))]
(-> (p/with-redefs [index/<index-list (fn [_db _user-id]
(p/resolved []))
index/<user-rsa-key-pair (fn [_db _user-id]
(p/resolved nil))]
(p/let [resp (index-handler/handle {:db :db
:env #js {}
:request request
:url url
:claims #js {"sub" "user-1"}
:route {:handler :graphs/list
:path-params {}}})
text (.text resp)
body (js->clj (js/JSON.parse text) :keywordize-keys true)]
(is (= 200 (.-status resp)))
(is (= [] (:graphs body)))
(is (= false (:user-rsa-keys-exists? body)))))
(p/then (fn []
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest graphs-create-e2ee-requires-user-rsa-key-pair-test
(async done
(let [request (js/Request. "http://localhost/graphs" #js {:method "POST"})
url (js/URL. (.-url request))
index-upsert-calls* (atom 0)]
(-> (p/with-redefs [common/read-json (fn [_]
(p/resolved #js {"graph-name" "Graph E2EE"
"schema-version" "65"
"graph-e2ee?" true}))
index/<graph-name-exists? (fn [_db _graph-name _user-id]
(p/resolved false))
index/<user-rsa-key-pair (fn [_db _user-id]
(p/resolved nil))
index/<index-upsert! (fn
([_db _graph-id _graph-name _user-id _schema-version _graph-e2ee?]
(swap! index-upsert-calls* inc)
(p/resolved nil))
([_db _graph-id _graph-name _user-id _schema-version _graph-e2ee? _graph-ready-for-use?]
(swap! index-upsert-calls* inc)
(p/resolved nil)))
index/<graph-member-upsert! (fn [& _]
(p/resolved nil))]
(p/let [resp (index-handler/handle {:db :db
:env #js {}
:request request
:url url
:claims #js {"sub" "user-1"}
:route {:handler :graphs/create
:path-params {}}})
text (.text resp)
body (js->clj (js/JSON.parse text) :keywordize-keys true)]
(is (= 400 (.-status resp)))
(is (= "missing user rsa key pair" (:error body)))
(is (zero? @index-upsert-calls*))))
(p/then (fn []
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest graphs-create-non-e2ee-skips-user-rsa-key-pair-test
(async done
(let [request (js/Request. "http://localhost/graphs" #js {:method "POST"})
url (js/URL. (.-url request))
index-upsert-calls* (atom 0)]
(-> (p/with-redefs [common/read-json (fn [_]
(p/resolved #js {"graph-name" "Graph Plain"
"schema-version" "65"
"graph-e2ee?" false}))
index/<graph-name-exists? (fn [_db _graph-name _user-id]
(p/resolved false))
index/<user-rsa-key-pair (fn [_db _user-id]
(p/resolved nil))
index/<index-upsert! (fn
([_db _graph-id _graph-name _user-id _schema-version _graph-e2ee?]
(swap! index-upsert-calls* inc)
(p/resolved nil))
([_db _graph-id _graph-name _user-id _schema-version _graph-e2ee? _graph-ready-for-use?]
(swap! index-upsert-calls* inc)
(p/resolved nil)))
index/<graph-member-upsert! (fn [& _]
(p/resolved nil))]
(p/let [resp (index-handler/handle {:db :db
:env #js {}
:request request
:url url
:claims #js {"sub" "user-1"}
:route {:handler :graphs/create
:path-params {}}})
text (.text resp)
body (js->clj (js/JSON.parse text) :keywordize-keys true)]
(is (= 200 (.-status resp)))
(is (string? (:graph-id body)))
(is (= false (:graph-e2ee? body)))
(is (= 1 @index-upsert-calls*))))
(p/then (fn []
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest graphs-delete-supports-node-delete-hook-without-do-namespace-test
(async done
(let [request (js/Request. "http://localhost/graphs/graph-1" #js {:method "DELETE"})
url (js/URL. (.-url request))
hook-calls* (atom [])]
(-> (p/with-redefs [index/<user-has-access-to-graph? (fn [_db _graph-id _user-id]
(p/resolved true))
index/<graph-delete-metadata! (fn [_db _graph-id]
(p/resolved true))
index/<graph-delete-index-entry! (fn [_db _graph-id]
(p/resolved true))]
(p/let [resp (index-handler/handle {:db :db
:env #js {"DB_SYNC_DELETE_GRAPH"
(fn [graph-id]
(swap! hook-calls* conj graph-id)
(p/resolved true))}
:request request
:url url
:claims #js {"sub" "user-1"}
:route {:handler :graphs/delete
:path-params {:graph-id "graph-1"}}})
text (.text resp)
body (js->clj (js/JSON.parse text) :keywordize-keys true)]
(is (= 200 (.-status resp)))
(is (= {:graph-id "graph-1" :deleted true} body))
(is (= ["graph-1"] @hook-calls*))))
(p/then (fn []
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,190 @@
(ns logseq.db-sync.worker-handler-ws-test
(:require [cljs-bean.core :as bean]
[cljs.test :refer [async deftest is]]
[datascript.core :as d]
[logseq.db-sync.protocol :as protocol]
[logseq.db-sync.storage :as storage]
[logseq.db-sync.test-sql :as test-sql]
[logseq.db-sync.worker.handler.sync :as sync-handler]
[logseq.db-sync.worker.handler.ws :as ws-handler]
[logseq.db-sync.worker.presence :as presence]
[logseq.db-sync.worker.ws :as ws]
[promesa.core :as p]))
(deftest presence-message-broadcast-excludes-source-client-test
(let [source-ws #js {:readyState 1}
peer-ws #js {:readyState 1}
send-events (atom [])
self #js {:state #js {:getWebSockets (fn [] #js [source-ws peer-ws])}}
raw (protocol/encode-message {:type "presence"
:editing-block-uuid "block-1"})]
(with-redefs [presence/get-user (fn [_self _ws] {:user-id "user-1"})
presence/update-presence! (fn [_self _ws _patch] nil)
ws/send! (fn [target msg]
(swap! send-events conj {:ws target
:msg msg}))]
(ws-handler/handle-ws-message! self source-ws raw))
(is (= [peer-ws]
(mapv :ws @send-events)))
(is (= [{:type "presence"
:editing-block-uuid "block-1"
:user-id "user-1"}]
(mapv :msg @send-events)))))
(deftest hello-message-includes-checksum-test
(let [sql (test-sql/make-sql)
conn (storage/open-conn sql)
ws #js {:readyState 1}
sent (atom nil)
self #js {:conn conn
:schema-ready true
:sql sql}
raw (protocol/encode-message {:type "hello"
:client "test"})]
(d/transact! conn [{:block/uuid (random-uuid)
:block/title "hello"}])
(with-redefs [ws/send! (fn [_target msg]
(reset! sent msg))]
(ws-handler/handle-ws-message! self ws raw))
(is (= "hello" (:type @sent)))
(is (number? (:t @sent)))
(is (string? (:checksum @sent)))))
(deftest hello-message-omits-nil-checksum-test
(let [sql (test-sql/make-sql)
conn (storage/open-conn sql)
ws #js {:readyState 1}
sent (atom nil)
self #js {:conn conn
:schema-ready true
:sql sql}
raw (protocol/encode-message {:type "hello"
:client "test"})]
(with-redefs [ws/send! (fn [_target msg]
(reset! sent msg))]
(ws-handler/handle-ws-message! self ws raw))
(is (= "hello" (:type @sent)))
(is (false? (contains? @sent :checksum)))))
(deftest tx-batch-message-adds-graph-and-user-context-to-transact-meta-test
(let [sql (test-sql/make-sql)
conn (storage/open-conn sql)
ws #js {:readyState 1}
sent (atom nil)
tx-metas (atom [])
self #js {:conn conn
:graph-id "graph-ws"
:schema-ready true
:sql sql}
raw (protocol/encode-message
{:type "tx/batch"
:client-revision "revision-ws"
:t-before 0
:txs [{:tx (protocol/tx->transit [[:db/add -1 :block/title "ws context"]])
:outliner-op :save-block}]})]
(with-redefs [presence/get-user (fn [_self _ws]
{:user-id "user-1"
:username "alice"})
ws/broadcast! (fn [& _] nil)
ws/send! (fn [_target msg]
(reset! sent msg))]
(d/listen! conn ::capture-ws-context-tx-meta
(fn [tx-report]
(swap! tx-metas conj (:tx-meta tx-report))))
(try
(ws-handler/handle-ws-message! self ws raw)
(finally
(d/unlisten! conn ::capture-ws-context-tx-meta))))
(is (= "tx/batch/ok" (:type @sent)))
(is (some #(= {:op :apply-client-tx
:outliner-op :save-block
:graph-id "graph-ws"
:client-revision "revision-ws"
:username "alice"}
(select-keys % [:op :outliner-op :graph-id :client-revision :username]))
@tx-metas))))
(deftest ws-send-serializes-tx-reject-uuids-as-strings-test
(let [raw* (atom nil)
ws #js {:readyState 1
:send (fn [raw] (reset! raw* raw))}
success-tx-id (random-uuid)
failed-tx-id (random-uuid)]
(ws/send! ws {:type "tx/reject"
:reason "db transact failed"
:t 3
:success-tx-ids [success-tx-id]
:failed-tx-id failed-tx-id})
(let [message (-> @raw* js/JSON.parse (js->clj :keywordize-keys true))]
(is (= "tx/reject" (:type message)))
(is (= [(str success-tx-id)] (:success-tx-ids message)))
(is (= (str failed-tx-id) (:failed-tx-id message))))))
(deftest online-users-broadcast-restored-attachment-user-test
(let [attachment* (atom nil)
ws #js {:readyState 1
:serializeAttachment (fn [attachment]
(reset! attachment* attachment))
:deserializeAttachment (fn []
@attachment*)}
self #js {}
restored-self #js {}
user {:user-id "user-1"
:email "user@example.com"
:username "alice"}
sent (atom nil)]
(presence/add-presence! self ws user)
(is (= {:presence/user user}
(bean/->clj (.deserializeAttachment ws))))
(swap! (presence/presence* restored-self)
assoc
ws
(presence/attachment->user (.deserializeAttachment ws)))
(with-redefs [ws/broadcast! (fn [_self _sender message]
(reset! sent message))]
(presence/broadcast-online-users! restored-self))
(is (= {:type "online-users"
:online-users [user]}
@sent))))
(deftest websocket-connection-is-rejected-while-snapshot-upload-is-in-progress-test
(async done
(let [accepted (atom [])
presence-events (atom [])
self #js {:state #js {:acceptWebSocket (fn [socket]
(swap! accepted conj socket))}}
request (js/Request. "http://localhost/sync/graph-1/ws?graph-id=graph-1"
#js {:method "GET"})]
(-> (p/with-redefs [sync-handler/<ready-for-sync? (fn [_ _] (p/resolved false))
presence/add-presence! (fn [& _]
(swap! presence-events conj :add))
presence/broadcast-online-users! (fn [& _]
(swap! presence-events conj :broadcast))]
(ws-handler/handle-ws self request))
(p/then (fn [response]
(is (= 409 (.-status response)))
(is (empty? @accepted))
(is (empty? @presence-events))
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))
(deftest websocket-connection-uses-graph-id-from-sync-path-test
(async done
(let [seen-graph-id (atom ::unset)
self #js {}
request (js/Request. "http://localhost/sync/graph-from-path"
#js {:method "GET"})]
(-> (p/with-redefs [sync-handler/<ready-for-sync? (fn [_self graph-id]
(reset! seen-graph-id graph-id)
(p/resolved false))]
(ws-handler/handle-ws self request))
(p/then (fn [response]
(is (= "graph-from-path" @seen-graph-id))
(is (= 409 (.-status response)))
(done)))
(p/catch (fn [error]
(is false (str error))
(done)))))))

View File

@@ -0,0 +1,216 @@
(ns logseq.db-sync.worker-large-op-memory-test
(:require ["better-sqlite3" :as sqlite3]
[clojure.string :as string]
[datascript.core :as d]
[logseq.db-sync.protocol :as protocol]
[logseq.db-sync.storage :as storage]
[logseq.db-sync.worker.handler.sync :as sync-handler]
[logseq.db-sync.worker.ws :as ws]
[logseq.db.frontend.validate :as db-validate]))
(def sqlite (if (find-ns 'nbb.core) (aget sqlite3 "default") sqlite3))
(defn- select-sql?
[sql]
(string/starts-with? (-> sql string/trim string/lower-case) "select"))
(defn- run-sql
[^js stmt args]
(.apply (.-run stmt) stmt (to-array args)))
(defn- all-sql
[^js stmt args]
(.apply (.-all stmt) stmt (to-array args)))
(defn- with-memory-sql
[f]
(let [db (new sqlite ":memory:" nil)
sql #js {:_db db
:exec (fn [sql-str & args]
(let [stmt (.prepare db sql-str)]
(if (select-sql? sql-str)
(all-sql stmt args)
(do
(run-sql stmt args)
nil))))
:close (fn []
(.close db))}]
(try
(f sql)
(finally
(.close sql)))))
(defn- large-block-insert-tx
[page-uuid block-count]
(let [block-uuids (mapv (fn [_idx] (random-uuid)) (range block-count))]
{:block-uuids block-uuids
:tx-data
(vec
(mapcat (fn [idx]
(let [block-uuid (nth block-uuids idx)
eid (str block-uuid)]
[[:db/add eid :block/uuid block-uuid idx]
[:db/add eid :block/title (str "large-memory-block-" idx) idx]
[:db/add eid :block/page [:block/uuid page-uuid] idx]
[:db/add eid :block/parent [:block/uuid page-uuid] idx]
[:db/add eid :block/order "a0" idx]
[:db/add eid :block/created-at idx idx]
[:db/add eid :block/updated-at idx idx]]))
(range block-count)))}))
(defn- sample-blocks-present?
[db block-uuids]
(every? (fn [idx]
(some? (d/entity db [:block/uuid (nth block-uuids idx)])))
[0 (quot (count block-uuids) 2) (dec (count block-uuids))]))
(defn- assert!
[condition message]
(when-not condition
(throw (js/Error. message))))
(defn- run-large-op-memory-test!
[{:keys [skip-final-store? skip-validation?]}]
(with-memory-sql
(fn [sql]
(storage/init-schema! sql)
(let [conn (storage/open-conn sql)
page-uuid (random-uuid)
_ (d/transact! conn [{:block/uuid page-uuid
:block/name "large-memory-page"
:block/title "large-memory-page"}])
t-before (storage/get-t sql)
{:keys [tx-data block-uuids]} (large-block-insert-tx page-uuid 2000)
tx-entry {:tx (protocol/tx->transit tx-data)
:tx-id (random-uuid)
:outliner-op :insert-blocks}
self #js {:sql sql
:conn conn
:schema-ready true}
tx-report-count (atom 0)
response (try
(d/listen! conn ::large-op-memory-chunks
(fn [_tx-report]
(swap! tx-report-count inc)))
(with-redefs [d/store (if skip-final-store?
(fn [_db] nil)
d/store)
db-validate/validate-tx-report
(if skip-validation?
(fn [_tx-report _options] [true nil])
db-validate/validate-tx-report)
ws/broadcast! (fn [& _] nil)]
(sync-handler/handle-tx-batch! self nil [tx-entry] t-before))
(finally
(d/unlisten! conn ::large-op-memory-chunks)))]
(assert! (= "tx/batch/ok" (:type response))
(str "expected tx/batch/ok, got " (pr-str response)))
(assert! (> @tx-report-count 1)
(str "expected more than one chunk, got " @tx-report-count))
(assert! (= (+ t-before @tx-report-count) (:t response))
(str "expected response t to advance by visible chunks, got " (:t response)))
(assert! (= @tx-report-count (count (storage/fetch-tx-since sql t-before)))
"expected large tx chunks to be persisted as visible tx log entries")
(assert! (sample-blocks-present? @conn block-uuids)
"expected sampled large tx blocks to be persisted")))))
(defn- run-client-split-large-op-memory-test!
[{:keys [serial?]}]
(with-memory-sql
(fn [sql]
(storage/init-schema! sql)
(let [conn (storage/open-conn sql)
page-uuid (random-uuid)
_ (d/transact! conn [{:block/uuid page-uuid
:block/name "large-memory-page"
:block/title "large-memory-page"}])
t-before (storage/get-t sql)
{:keys [tx-data block-uuids]} (large-block-insert-tx page-uuid 2000)
tx-id (random-uuid)
tx-entries (mapv (fn [chunk]
{:tx (protocol/tx->transit chunk)
:tx-id tx-id
:outliner-op :insert-blocks})
(partition-all 392 tx-data))
self #js {:sql sql
:conn conn
:schema-ready true}
tx-report-count (atom 0)
response (try
(d/listen! conn ::large-op-memory-client-split
(fn [_tx-report]
(swap! tx-report-count inc)))
(with-redefs [ws/broadcast! (fn [& _] nil)]
(if serial?
(loop [t-before* t-before
[tx-entry & more] tx-entries
response nil]
(if tx-entry
(let [response* (sync-handler/handle-tx-batch! self nil [tx-entry] t-before*)]
(assert! (= "tx/batch/ok" (:type response*))
(str "expected tx/batch/ok, got " (pr-str response*)))
(recur (:t response*) more response*))
response))
(sync-handler/handle-tx-batch! self nil tx-entries t-before)))
(finally
(d/unlisten! conn ::large-op-memory-client-split)))]
(assert! (= "tx/batch/ok" (:type response))
(str "expected tx/batch/ok, got " (pr-str response)))
(assert! (= (count tx-entries) @tx-report-count)
(str "expected one tx report per client split entry, got " @tx-report-count))
(assert! (= (+ t-before @tx-report-count) (:t response))
(str "expected response t to advance by client split entries, got " (:t response)))
(assert! (= @tx-report-count (count (storage/fetch-tx-since sql t-before)))
"expected client split tx entries to be persisted as visible tx log entries")
(assert! (sample-blocks-present? @conn block-uuids)
"expected sampled client split blocks to be persisted")))))
(defn- run-open-sql-memory-test!
[]
(with-memory-sql
(fn [sql]
(storage/init-schema! sql)
(let [conn (storage/open-conn sql)
page-uuid (random-uuid)]
(d/transact! conn [{:block/uuid page-uuid
:block/name "large-memory-page"
:block/title "large-memory-page"}])
(assert! (some? (d/entity @conn [:block/uuid page-uuid]))
"expected setup page to be persisted")))))
(defn- run-prepare-memory-test!
[]
(let [page-uuid (random-uuid)
{:keys [tx-data]} (large-block-insert-tx page-uuid 2000)
tx-entry {:tx (protocol/tx->transit tx-data)
:tx-id (random-uuid)
:outliner-op :insert-blocks}]
(assert! (= 14000 (count tx-data))
"expected generated tx data")
(assert! (string? (:tx tx-entry))
"expected transit tx payload")))
(defn- run-mode!
[mode]
(case mode
"baseline" nil
"open-sql" (run-open-sql-memory-test!)
"prepare" (run-prepare-memory-test!)
"apply" (run-large-op-memory-test! nil)
"apply-client-split" (run-client-split-large-op-memory-test! nil)
"apply-client-split-serial" (run-client-split-large-op-memory-test! {:serial? true})
"apply-no-store" (run-large-op-memory-test! {:skip-final-store? true})
"apply-no-validation" (run-large-op-memory-test! {:skip-validation? true})
"apply-no-store-validation" (run-large-op-memory-test! {:skip-final-store? true
:skip-validation? true})
(run-large-op-memory-test! nil)))
(defn main [& args]
(try
(let [mode (or (first args) "apply")]
(run-mode! mode)
(js/console.log (str "large op memory test passed: " mode)))
(js/process.exit 0)
(catch :default error
(js/console.error error)
(js/process.exit 1))))

View File

@@ -0,0 +1,89 @@
(ns logseq.db-sync.worker-routes-test
(:require [cljs.test :refer [deftest is testing]]
[logseq.db-sync.worker.routes.index :as routes]
[logseq.db-sync.worker.routes.semantic :as semantic-routes]))
(deftest match-route-graphs-test
(testing "graphs routes"
(let [match (routes/match-route "GET" "/graphs")]
(is (= :graphs/list (:handler match))))
(let [match (routes/match-route "POST" "/graphs")]
(is (= :graphs/create (:handler match))))
(let [match (routes/match-route "GET" "/graphs/graph-1/access")]
(is (= :graphs/access (:handler match)))
(is (= "graph-1" (get-in match [:path-params :graph-id]))))
(let [match (routes/match-route "GET" "/graphs/graph-2/members")]
(is (= :graph-members/list (:handler match)))
(is (= "graph-2" (get-in match [:path-params :graph-id]))))
(let [match (routes/match-route "POST" "/graphs/graph-3/members")]
(is (= :graph-members/create (:handler match))))
(let [match (routes/match-route "PUT" "/graphs/graph-4/members/user-9")]
(is (= :graph-members/update (:handler match)))
(is (= "graph-4" (get-in match [:path-params :graph-id])))
(is (= "user-9" (get-in match [:path-params :member-id]))))
(let [match (routes/match-route "DELETE" "/graphs/graph-5")]
(is (= :graphs/delete (:handler match)))
(is (= "graph-5" (get-in match [:path-params :graph-id]))))))
(deftest match-route-e2ee-test
(testing "e2ee routes"
(let [match (routes/match-route "GET" "/e2ee/user-keys")]
(is (= :e2ee/user-keys-get (:handler match))))
(let [match (routes/match-route "POST" "/e2ee/user-keys")]
(is (= :e2ee/user-keys-post (:handler match))))
(let [match (routes/match-route "GET" "/e2ee/user-public-key")]
(is (= :e2ee/user-public-key-get (:handler match))))
(let [match (routes/match-route "GET" "/e2ee/graphs/graph-7/aes-key")]
(is (= :e2ee/graph-aes-key-get (:handler match)))
(is (= "graph-7" (get-in match [:path-params :graph-id]))))
(let [match (routes/match-route "POST" "/e2ee/graphs/graph-8/aes-key")]
(is (= :e2ee/graph-aes-key-post (:handler match)))
(is (= "graph-8" (get-in match [:path-params :graph-id]))))
(let [match (routes/match-route "POST" "/e2ee/graphs/graph-9/grant-access")]
(is (= :e2ee/grant-access (:handler match)))
(is (= "graph-9" (get-in match [:path-params :graph-id]))))))
(deftest match-route-method-mismatch-test
(testing "method mismatch returns nil"
(is (nil? (routes/match-route "GET" "/graphs/graph-1/members/user-9")))
(is (nil? (routes/match-route "PUT" "/e2ee/user-keys")))))
(deftest semantic-internal-routes-ignore-public-only-operations-test
(is (nil? (semantic-routes/match-internal "GET" "/api/v1/graphs")))
(is (= :semantic/pages-list
(:handler (semantic-routes/match-internal "GET" "/semantic/pages")))))
(deftest semantic-move-blocks-route-test
(is (= :semantic/blocks-move
(:handler (semantic-routes/match-public "POST" "/api/v1/graphs/graph-1/block-moves"))))
(is (= :semantic/blocks-move
(:handler (semantic-routes/match-internal "POST" "/semantic/block-moves"))))
(is (nil? (semantic-routes/match-public "POST" "/api/v1/graphs/graph-1/blocks/block-1/move"))))
(deftest semantic-reverse-reference-routes-test
(is (= :semantic/tags-objects
(:handler (semantic-routes/match-public
"GET" "/api/v1/graphs/graph-1/tags/tag-1/objects"))))
(is (= :semantic/tags-objects
(:handler (semantic-routes/match-internal "GET" "/semantic/tags/tag-1/objects"))))
(is (= :semantic/pages-references
(:handler (semantic-routes/match-public
"GET" "/api/v1/graphs/graph-1/pages/page-1/references"))))
(is (= :semantic/pages-references
(:handler (semantic-routes/match-internal "GET" "/semantic/pages/page-1/references")))))
(deftest semantic-task-routes-test
(doseq [[method handler] [["GET" :semantic/tasks-list]
["POST" :semantic/tasks-create]]]
(is (= handler
(:handler (semantic-routes/match-public method "/api/v1/graphs/graph-1/tasks"))))
(is (= handler
(:handler (semantic-routes/match-internal method "/semantic/tasks"))))))
(deftest semantic-asset-collection-routes-test
(doseq [[method handler] [["GET" :semantic/assets-list]
["POST" :semantic/assets-create]]]
(is (= handler
(:handler (semantic-routes/match-public method "/api/v1/graphs/graph-1/assets"))))
(is (= handler
(:handler (semantic-routes/match-internal method "/semantic/assets"))))))

View File

@@ -0,0 +1,27 @@
(ns logseq.db-sync.worker-sync-routes-test
(:require [cljs.test :refer [deftest is testing]]
[logseq.db-sync.worker.routes.sync :as sync-routes]))
(deftest match-route-sync-test
(testing "sync routes"
(let [match (sync-routes/match-route "GET" "/health")]
(is (= :sync/health (:handler match))))
(let [match (sync-routes/match-route "GET" "/pull")]
(is (= :sync/pull (:handler match))))
(is (nil? (sync-routes/match-route "GET" "/repair/blocks")))
(let [match (sync-routes/match-route "GET" "/snapshot/download")]
(is (= :sync/snapshot-download (:handler match))))
(let [match (sync-routes/match-route "GET" "/snapshot/stream")]
(is (= :sync/snapshot-stream (:handler match))))
(let [match (sync-routes/match-route "DELETE" "/admin/reset")]
(is (= :sync/admin-reset (:handler match))))
(let [match (sync-routes/match-route "POST" "/tx/batch")]
(is (= :sync/tx-batch (:handler match))))
(let [match (sync-routes/match-route "POST" "/snapshot/upload")]
(is (= :sync/snapshot-upload (:handler match))))))
(deftest match-route-sync-method-mismatch-test
(testing "sync method mismatch returns nil"
(is (nil? (sync-routes/match-route "POST" "/health")))
(is (nil? (sync-routes/match-route "GET" "/admin/reset")))
(is (nil? (sync-routes/match-route "PUT" "/tx/batch")))))

View File

@@ -0,0 +1,30 @@
(ns logseq.db-sync.worker-test
(:require [cljs.test :refer [deftest is]]
[datascript.core :as d]
[logseq.db-sync.order :as sync-order]
[logseq.db.common.order :as db-order]
[logseq.db.frontend.schema :as db-schema]))
(defn- new-conn []
(d/create-conn db-schema/schema))
(deftest duplicate-order-fix-test
(let [conn (new-conn)
parent (random-uuid)
block-a (random-uuid)
block-b (random-uuid)
order-a (db-order/gen-key nil nil)
order-b (db-order/gen-key order-a nil)]
(d/transact! conn [{:block/uuid parent}
{:block/uuid block-a
:block/parent [:block/uuid parent]
:block/order order-a}
{:block/uuid block-b
:block/parent [:block/uuid parent]
:block/order order-b}])
(let [tx [[:db/add [:block/uuid block-b] :block/order order-a]]
_ (sync-order/fix-duplicate-orders! conn tx {})
order-a' (:block/order (d/entity @conn [:block/uuid block-a]))
order-b' (:block/order (d/entity @conn [:block/uuid block-b]))]
(is (= order-a order-a'))
(is (not= order-a' order-b')))))