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:
17
deps/db-sync/test/logseq/db_sync/batch_test.cljs
vendored
Normal file
17
deps/db-sync/test/logseq/db_sync/batch_test.cljs
vendored
Normal 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})))))
|
||||
547
deps/db-sync/test/logseq/db_sync/checksum_test.cljs
vendored
Normal file
547
deps/db-sync/test/logseq/db_sync/checksum_test.cljs
vendored
Normal 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)))))
|
||||
91
deps/db-sync/test/logseq/db_sync/common_test.cljs
vendored
Normal file
91
deps/db-sync/test/logseq/db_sync/common_test.cljs
vendored
Normal 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)))))))
|
||||
229
deps/db-sync/test/logseq/db_sync/cycle_test.cljs
vendored
Normal file
229
deps/db-sync/test/logseq/db_sync/cycle_test.cljs
vendored
Normal 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))))))
|
||||
1
deps/db-sync/test/logseq/db_sync/fixtures/parent_order_rebase_checksum_payload.edn
vendored
Normal file
1
deps/db-sync/test/logseq/db_sync/fixtures/parent_order_rebase_checksum_payload.edn
vendored
Normal file
File diff suppressed because one or more lines are too long
1
deps/db-sync/test/logseq/db_sync/fixtures/rebased_retract_checksum_payload.edn
vendored
Normal file
1
deps/db-sync/test/logseq/db_sync/fixtures/rebased_retract_checksum_payload.edn
vendored
Normal file
File diff suppressed because one or more lines are too long
197
deps/db-sync/test/logseq/db_sync/index_test.cljs
vendored
Normal file
197
deps/db-sync/test/logseq/db_sync/index_test.cljs
vendored
Normal 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)))))))
|
||||
42
deps/db-sync/test/logseq/db_sync/malli_schema_test.cljs
vendored
Normal file
42
deps/db-sync/test/logseq/db_sync/malli_schema_test.cljs
vendored
Normal 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)))))
|
||||
108
deps/db-sync/test/logseq/db_sync/node_adapter_test.cljs
vendored
Normal file
108
deps/db-sync/test/logseq/db_sync/node_adapter_test.cljs
vendored
Normal 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)))))))))
|
||||
31
deps/db-sync/test/logseq/db_sync/node_config_test.cljs
vendored
Normal file
31
deps/db-sync/test/logseq/db_sync/node_config_test.cljs
vendored
Normal 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"})))))
|
||||
163
deps/db-sync/test/logseq/db_sync/node_server_test.cljs
vendored
Normal file
163
deps/db-sync/test/logseq/db_sync/node_server_test.cljs
vendored
Normal 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)))))))))
|
||||
382
deps/db-sync/test/logseq/db_sync/normalize_test.cljs
vendored
Normal file
382
deps/db-sync/test/logseq/db_sync/normalize_test.cljs
vendored
Normal 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))))))))
|
||||
272
deps/db-sync/test/logseq/db_sync/storage_test.cljs
vendored
Normal file
272
deps/db-sync/test/logseq/db_sync/storage_test.cljs
vendored
Normal 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))))))))
|
||||
36
deps/db-sync/test/logseq/db_sync/test_runner.cljs
vendored
Normal file
36
deps/db-sync/test/logseq/db_sync/test_runner.cljs
vendored
Normal 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)))
|
||||
56
deps/db-sync/test/logseq/db_sync/test_sql.cljs
vendored
Normal file
56
deps/db-sync/test/logseq/db_sync/test_sql.cljs
vendored
Normal 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))}))
|
||||
104
deps/db-sync/test/logseq/db_sync/tx_sanitize_test.cljs
vendored
Normal file
104
deps/db-sync/test/logseq/db_sync/tx_sanitize_test.cljs
vendored
Normal 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)))))
|
||||
107
deps/db-sync/test/logseq/db_sync/worker_auth_test.cljs
vendored
Normal file
107
deps/db-sync/test/logseq/db_sync/worker_auth_test.cljs
vendored
Normal 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)))))))
|
||||
392
deps/db-sync/test/logseq/db_sync/worker_dispatch_test.cljs
vendored
Normal file
392
deps/db-sync/test/logseq/db_sync/worker_dispatch_test.cljs
vendored
Normal 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)))))))
|
||||
144
deps/db-sync/test/logseq/db_sync/worker_handler_assets_test.cljs
vendored
Normal file
144
deps/db-sync/test/logseq/db_sync/worker_handler_assets_test.cljs
vendored
Normal 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)))))))
|
||||
260
deps/db-sync/test/logseq/db_sync/worker_handler_index_test.cljs
vendored
Normal file
260
deps/db-sync/test/logseq/db_sync/worker_handler_index_test.cljs
vendored
Normal 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)))))))
|
||||
3041
deps/db-sync/test/logseq/db_sync/worker_handler_sync_test.cljs
vendored
Normal file
3041
deps/db-sync/test/logseq/db_sync/worker_handler_sync_test.cljs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
190
deps/db-sync/test/logseq/db_sync/worker_handler_ws_test.cljs
vendored
Normal file
190
deps/db-sync/test/logseq/db_sync/worker_handler_ws_test.cljs
vendored
Normal 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)))))))
|
||||
216
deps/db-sync/test/logseq/db_sync/worker_large_op_memory_test.cljs
vendored
Normal file
216
deps/db-sync/test/logseq/db_sync/worker_large_op_memory_test.cljs
vendored
Normal 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))))
|
||||
89
deps/db-sync/test/logseq/db_sync/worker_routes_test.cljs
vendored
Normal file
89
deps/db-sync/test/logseq/db_sync/worker_routes_test.cljs
vendored
Normal 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"))))))
|
||||
27
deps/db-sync/test/logseq/db_sync/worker_sync_routes_test.cljs
vendored
Normal file
27
deps/db-sync/test/logseq/db_sync/worker_sync_routes_test.cljs
vendored
Normal 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")))))
|
||||
30
deps/db-sync/test/logseq/db_sync/worker_test.cljs
vendored
Normal file
30
deps/db-sync/test/logseq/db_sync/worker_test.cljs
vendored
Normal 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')))))
|
||||
Reference in New Issue
Block a user