1550 lines
95 KiB
Clojure
1550 lines
95 KiB
Clojure
(ns metabase-enterprise.remote-sync.events-test
|
|
"Tests for the remote-sync events system.
|
|
|
|
Tests event publishing, handling, and model change tracking functionality."
|
|
{:clj-kondo/config '{:linters {:deprecated-var {:exclude {metabase.test.data/mbql-query {:namespaces [metabase-enterprise.remote-sync.events-test]}}}}}}
|
|
(:require
|
|
[clojure.test :refer :all]
|
|
[java-time.api :as t]
|
|
[metabase-enterprise.remote-sync.core :as remote-sync.core]
|
|
[metabase-enterprise.remote-sync.events :as remote-sync.events]
|
|
[metabase-enterprise.remote-sync.spec :as spec]
|
|
[metabase.collections.models.collection :as collection]
|
|
[metabase.collections.test-utils :refer [with-library-synced]]
|
|
[metabase.events.core :as events]
|
|
[metabase.test :as mt]
|
|
[metabase.test.fixtures :as fixtures]
|
|
[metabase.test.util.thread-local :as tu.thread-local]
|
|
[metabase.util :as u]
|
|
[toucan2.core :as t2]))
|
|
|
|
(use-fixtures :once (fixtures/initialize :db))
|
|
(defn- mark-synced!
|
|
"Populate the RemoteSyncObject table with synced rows for `imported-data` (a `:by-entity-id` map), as a
|
|
successful import does — used to set up a synced baseline before firing an event."
|
|
[imported-data]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(when-let [rows (seq (spec/sync-all-entities! (t/instant) imported-data))]
|
|
(t2/insert! :model/RemoteSyncObject rows)))
|
|
|
|
;;; Model Change Event Tests
|
|
|
|
(deftest card-create-event-creates-entry-test
|
|
(testing "card-create event creates remote sync object entry with create status"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Card remote-sync-card {:name "Test Card"
|
|
:dataset_query (mt/mbql-query venues)
|
|
:collection_id (:id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/card-create
|
|
{:object remote-sync-card :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Card"
|
|
:model_id (:id remote-sync-card)
|
|
:status "create"}
|
|
(first entries))))))))
|
|
|
|
(deftest card-update-event-creates-entry-test
|
|
(testing "card-update event creates or updates remote sync object entry with update status"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Card remote-sync-card {:name "Test Card"
|
|
:dataset_query (mt/mbql-query venues)
|
|
:collection_id (:id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/card-update
|
|
{:object remote-sync-card :previous-object remote-sync-card :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Card"
|
|
:model_id (:id remote-sync-card)
|
|
:status "update"}
|
|
(first entries))))))))
|
|
|
|
(deftest card-update-archived-sets-delete-test
|
|
(testing "card-update event with archived=true sets delete status"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Card remote-sync-card {:name "Test Card"
|
|
:dataset_query (mt/mbql-query venues)
|
|
:collection_id (:id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/card-update
|
|
{:object (assoc remote-sync-card :archived true)
|
|
:previous-object remote-sync-card
|
|
:user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Card"
|
|
:model_id (:id remote-sync-card)
|
|
:status "delete"}
|
|
(first entries))))))))
|
|
|
|
(deftest card-delete-event-creates-entry-test
|
|
(testing "card-delete event creates remote sync object entry with delete status"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Card remote-sync-card {:name "Test Card"
|
|
:dataset_query (mt/mbql-query venues)
|
|
:collection_id (:id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/card-delete
|
|
{:object remote-sync-card :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Card"
|
|
:model_id (:id remote-sync-card)
|
|
:status "delete"}
|
|
(first entries))))))))
|
|
|
|
(deftest card-multiple-events-update-same-object-test
|
|
(testing "multiple card events do not update object when status is create"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Card remote-sync-card {:name "Test Card"
|
|
:dataset_query (mt/mbql-query venues)
|
|
:collection_id (:id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(let [clock-t1 (t/mock-clock (t/instant "2024-01-01T10:00:00Z") (t/zone-id "UTC"))]
|
|
(t/with-clock clock-t1
|
|
(events/publish-event! :event/card-create
|
|
{:object remote-sync-card :user-id (mt/user->id :rasta)}))
|
|
(let [initial-entry (t2/select-one :model/RemoteSyncObject
|
|
:model_type "Card"
|
|
:model_id (:id remote-sync-card))]
|
|
(let [clock-t2 (t/mock-clock (t/instant "2024-01-01T11:00:00Z") (t/zone-id "UTC"))]
|
|
(t/with-clock clock-t2
|
|
(events/publish-event! :event/card-update
|
|
{:object remote-sync-card :previous-object remote-sync-card :user-id (mt/user->id :rasta)})))
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(let [update-entry (first entries)]
|
|
(is (= (:id initial-entry) (:id update-entry)))
|
|
(is (= "create" (:status update-entry)))
|
|
(is (= (:status_changed_at initial-entry) (:status_changed_at update-entry)))))))))))
|
|
|
|
(deftest card-event-in-normal-collection-no-entry-test
|
|
(testing "card events in non-remote-sync collections don't create entries"
|
|
(mt/with-temp [:model/Collection normal-collection {:name "Normal"}
|
|
:model/Card normal-card {:name "Normal Card"
|
|
:dataset_query (mt/mbql-query venues)
|
|
:collection_id (:id normal-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/card-create
|
|
{:object normal-card :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 0 (count entries)))))))
|
|
|
|
(defn- action-rso [action-id]
|
|
(t2/select-one :model/RemoteSyncObject :model_type "Action" :model_id action-id))
|
|
|
|
(deftest action-create-event-creates-entry-test
|
|
(testing "action-create on a synced model tracks the action under its model's collection"
|
|
;; remote-sync-type defaults to :read-only, where synced items are not tracked
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection {coll-id :id} {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Card {model-id :id} {:type :model :collection_id coll-id}
|
|
:model/Action action {:type :implicit :name "Create Venue" :model_id model-id}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/action-create {:object action :user-id (mt/user->id :rasta)})
|
|
(is (=? {:status "create" :model_name "Create Venue" :model_collection_id coll-id}
|
|
(action-rso (:id action))))))))
|
|
|
|
(deftest action-event-on-unsynced-model-no-entry-test
|
|
(testing "GHY-4722: action events on a model outside synced collections are not tracked"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection {coll-id :id} {:name "Normal"}
|
|
:model/Card {model-id :id} {:type :model :collection_id coll-id}
|
|
:model/Action action {:type :implicit :name "Create Venue" :model_id model-id}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/action-create {:object action :user-id (mt/user->id :rasta)})
|
|
(is (nil? (action-rso (:id action))))))))
|
|
|
|
(deftest action-moved-with-its-model-test
|
|
(testing "an action that moves out of synced collections with its model is marked removed"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection {synced-id :id} {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Collection {plain-id :id} {:name "Normal"}
|
|
:model/Card {model-id :id} {:type :model :collection_id synced-id}
|
|
:model/Action {action-id :id} {:type :implicit :name "Create Venue" :model_id model-id}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(t2/insert! :model/RemoteSyncObject {:model_type "Action" :model_id action-id :model_name "Create Venue"
|
|
:model_collection_id synced-id :status "synced"
|
|
:status_changed_at (t/offset-date-time)})
|
|
(t2/update! :model/Card model-id {:collection_id plain-id})
|
|
(events/publish-event! :event/action-update {:object (t2/select-one :model/Action :id action-id)
|
|
:user-id (mt/user->id :rasta)})
|
|
(is (=? {:status "removed"} (action-rso action-id)))))))
|
|
|
|
(deftest dashboard-create-event-creates-entry-test
|
|
(testing "dashboard-create event creates remote sync object entry with create status"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Dashboard dashboard {:name "Test Dashboard"
|
|
:collection_id (:id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/dashboard-create
|
|
{:object dashboard :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Dashboard"
|
|
:model_id (:id dashboard)
|
|
:status "create"}
|
|
(first entries))))))))
|
|
|
|
(deftest dashboard-update-event-creates-entry-test
|
|
(testing "dashboard-update event creates or updates remote sync object entry"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Dashboard dashboard {:name "Test Dashboard"
|
|
:collection_id (:id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/dashboard-update
|
|
{:object dashboard :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Dashboard"
|
|
:model_id (:id dashboard)
|
|
:status "update"}
|
|
(first entries))))))))
|
|
|
|
(deftest public-link-events-create-update-entry-test
|
|
(testing "GHY-4650: on a read-write instance, public-link events mark the card or dashboard as updated"
|
|
(doseq [[model topics] {:model/Card [:event/card-public-link-created :event/card-public-link-deleted]
|
|
:model/Dashboard [:event/dashboard-public-link-created :event/dashboard-public-link-deleted]}
|
|
topic topics]
|
|
(testing topic
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
model instance {:name "Shared" :collection_id (:id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! topic {:object instance :object-id (:id instance) :user-id (mt/user->id :rasta)})
|
|
(is (=? [{:model_type (name model)
|
|
:model_id (:id instance)
|
|
:status "update"}]
|
|
(t2/select :model/RemoteSyncObject)))))))))
|
|
|
|
(deftest events-on-read-only-instance-do-not-track-synced-items-test
|
|
(testing "On a read-only instance, no event marks a synced item dirty, since that change can never be pushed"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-only]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Dashboard dashboard {:name "Test Dashboard" :collection_id (:id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/dashboard-update {:object dashboard :user-id (mt/user->id :rasta)})
|
|
(is (empty? (t2/select :model/RemoteSyncObject)))))))
|
|
|
|
(deftest public-link-events-on-read-only-instance-test
|
|
(testing "GHY-4650: on a read-only instance, public-link events leave synced cards and dashboards untracked"
|
|
(doseq [[model topics] {:model/Card [:event/card-public-link-created :event/card-public-link-deleted]
|
|
:model/Dashboard [:event/dashboard-public-link-created :event/dashboard-public-link-deleted]}
|
|
topic topics]
|
|
(testing topic
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-only]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
model instance {:name "Shared" :collection_id (:id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! topic {:object instance :object-id (:id instance) :user-id (mt/user->id :rasta)})
|
|
(is (empty? (t2/select :model/RemoteSyncObject)))))))))
|
|
|
|
(deftest dashboard-update-archived-sets-delete-test
|
|
(testing "dashboard-update event with archived=true sets delete status"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Dashboard dashboard {:name "Test Dashboard"
|
|
:collection_id (:id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/dashboard-update
|
|
{:object (assoc dashboard :archived true)
|
|
:user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Dashboard"
|
|
:model_id (:id dashboard)
|
|
:status "delete"}
|
|
(first entries))))))))
|
|
|
|
(deftest dashboard-delete-event-creates-entry-test
|
|
(testing "dashboard-delete event creates remote sync object entry with delete status"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Dashboard dashboard {:name "Test Dashboard"
|
|
:collection_id (:id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/dashboard-delete
|
|
{:object dashboard :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Dashboard"
|
|
:model_id (:id dashboard)
|
|
:status "delete"}
|
|
(first entries))))))))
|
|
|
|
(deftest document-create-event-creates-entry-test
|
|
(testing "document-create event creates remote sync object entry with create status"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Document document {:collection_id (u/the-id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/document-create
|
|
{:object document :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Document"
|
|
:model_id (:id document)
|
|
:status "create"}
|
|
(first entries))))))))
|
|
|
|
(deftest document-update-event-creates-entry-test
|
|
(testing "document-update event creates or updates remote sync object entry with update status"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Document document {:collection_id (u/the-id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/document-update
|
|
{:object document :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Document"
|
|
:model_id (:id document)
|
|
:status "update"}
|
|
(first entries))))))))
|
|
|
|
(deftest document-update-archived-sets-delete-test
|
|
(testing "document-update event with archived=true sets delete status"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Document document {:collection_id (u/the-id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/document-update
|
|
{:object (assoc document :archived true)
|
|
:user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Document"
|
|
:model_id (:id document)
|
|
:status "delete"}
|
|
(first entries))))))))
|
|
|
|
(deftest document-delete-event-creates-entry-test
|
|
(testing "document-delete event creates remote sync object entry with delete status"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Document document {:collection_id (u/the-id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/document-delete
|
|
{:object document :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Document"
|
|
:model_id (:id document)
|
|
:status "delete"}
|
|
(first entries))))))))
|
|
|
|
(deftest snippet-create-event-creates-entry-test
|
|
(testing "snippet-create event creates remote sync object entry with create status when Library is synced"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(with-library-synced
|
|
(mt/with-temp [:model/Collection snippet-collection {:name "Snippets" :namespace "snippets"}
|
|
:model/NativeQuerySnippet snippet {:name "Test Snippet"
|
|
:content "SELECT 1"
|
|
:collection_id (:id snippet-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/snippet-create
|
|
{:object snippet :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "NativeQuerySnippet"
|
|
:model_id (:id snippet)
|
|
:status "create"}
|
|
(first entries)))))))))
|
|
|
|
(deftest snippet-update-event-creates-entry-test
|
|
(testing "snippet-update event creates or updates remote sync object entry with update status when Library is synced"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(with-library-synced
|
|
(mt/with-temp [:model/Collection snippet-collection {:name "Snippets" :namespace "snippets"}
|
|
:model/NativeQuerySnippet snippet {:name "Test Snippet"
|
|
:content "SELECT 1"
|
|
:collection_id (:id snippet-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/snippet-update
|
|
{:object snippet :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "NativeQuerySnippet"
|
|
:model_id (:id snippet)
|
|
:status "update"}
|
|
(first entries)))))))))
|
|
|
|
(deftest snippet-update-archived-sets-delete-test
|
|
(testing "snippet-update event with archived=true sets delete status when Library is synced"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(with-library-synced
|
|
(mt/with-temp [:model/Collection snippet-collection {:name "Snippets" :namespace "snippets"}
|
|
:model/NativeQuerySnippet snippet {:name "Test Snippet"
|
|
:content "SELECT 1"
|
|
:collection_id (:id snippet-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/snippet-update
|
|
{:object (assoc snippet :archived true)
|
|
:user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "NativeQuerySnippet"
|
|
:model_id (:id snippet)
|
|
:status "delete"}
|
|
(first entries)))))))))
|
|
|
|
(deftest snippet-delete-event-creates-entry-test
|
|
(testing "snippet-delete event creates remote sync object entry with delete status when Library is synced"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(with-library-synced
|
|
(mt/with-temp [:model/Collection snippet-collection {:name "Snippets" :namespace "snippets"}
|
|
:model/NativeQuerySnippet snippet {:name "Test Snippet"
|
|
:content "SELECT 1"
|
|
:collection_id (:id snippet-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/snippet-delete
|
|
{:object snippet :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "NativeQuerySnippet"
|
|
:model_id (:id snippet)
|
|
:status "delete"}
|
|
(first entries)))))))))
|
|
|
|
(deftest snippet-event-when-library-not-synced-no-entry-test
|
|
(testing "snippet events don't create entries when Library is not synced"
|
|
(when-let [library (collection/library-collection)]
|
|
(t2/update! :model/Collection (:id library) {:is_remote_synced false}))
|
|
(mt/with-temp [:model/Collection snippet-collection {:name "Snippets" :namespace "snippets"}
|
|
:model/NativeQuerySnippet snippet {:name "Test Snippet"
|
|
:content "SELECT 1"
|
|
:collection_id (:id snippet-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/snippet-create
|
|
{:object snippet :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 0 (count entries)))))))
|
|
|
|
(deftest collection-create-event-creates-entry-test
|
|
(testing "collection-create event creates remote sync object entry with create status"
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true
|
|
:name "Remote-Sync"}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/collection-create
|
|
{:object remote-sync-collection :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Collection"
|
|
:model_id (:id remote-sync-collection)
|
|
:status "create"}
|
|
(first entries)))))))
|
|
|
|
(deftest collection-create-event-no-entry-for-normal-collection-test
|
|
(testing "collection-create event doesn't create entry for non-remote-synced collections"
|
|
(mt/with-temp [:model/Collection normal-collection {:name "Normal"}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/collection-create
|
|
{:object normal-collection :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 0 (count entries)))))))
|
|
|
|
(deftest collection-create-event-archived-sets-delete-test
|
|
(testing "collection-create event with archived=true sets delete status"
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true
|
|
:name "Remote-Sync"}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/collection-create
|
|
{:object (assoc remote-sync-collection :archived true)
|
|
:user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Collection"
|
|
:model_id (:id remote-sync-collection)
|
|
:status "delete"}
|
|
(first entries)))))))
|
|
|
|
(deftest collection-update-event-creates-entry-test
|
|
(testing "collection-update event creates or updates remote sync object entry with update status"
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true
|
|
:name "Remote-Sync"}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/collection-update
|
|
{:object remote-sync-collection :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Collection"
|
|
:model_id (:id remote-sync-collection)
|
|
:status "update"}
|
|
(first entries)))))))
|
|
|
|
(deftest collection-update-event-archived-sets-delete-test
|
|
(testing "collection-update event with archived=true sets delete status"
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true
|
|
:name "Remote-Sync"}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/collection-update
|
|
{:object (assoc remote-sync-collection :archived true)
|
|
:user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Collection"
|
|
:model_id (:id remote-sync-collection)
|
|
:status "delete"}
|
|
(first entries)))))))
|
|
|
|
(deftest collection-update-event-unarchived-sets-update-test
|
|
(testing "collection-update event with archived=false sets update status"
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true
|
|
:name "Remote-Sync"}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/collection-update
|
|
{:object (assoc remote-sync-collection :archived false)
|
|
:user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Collection"
|
|
:model_id (:id remote-sync-collection)
|
|
:status "update"}
|
|
(first entries)))))))
|
|
|
|
(defn- rso-statuses
|
|
"`{[model_type model_id] status}` for every RemoteSyncObject."
|
|
[]
|
|
(into {} (map (juxt (juxt :model_type :model_id) :status)) (t2/select :model/RemoteSyncObject)))
|
|
|
|
(defn- with-synced-nested-subtree!
|
|
"Calls `f` with `{:root :folder :sub-folder :card :sub-card :sibling-card}` ids: a remote-synced root holding a
|
|
sibling card and a folder, which holds a card and a sub-folder with its own card. Every entity except the root
|
|
has a synced RemoteSyncObject row."
|
|
[f]
|
|
(mt/with-temp [:model/Collection {root :id} {:name "Root" :is_remote_synced true :location "/"}
|
|
:model/Collection {folder :id} {:name "Folder" :is_remote_synced true :location (str "/" root "/")}
|
|
:model/Collection {sub-folder :id} {:name "Sub" :is_remote_synced true :location (str "/" root "/" folder "/")}
|
|
:model/Card {card :id} {:name "Card" :collection_id folder}
|
|
:model/Card {sub-card :id} {:name "Sub Card" :collection_id sub-folder}
|
|
:model/Card {sibling-card :id} {:name "Sibling Card" :collection_id root}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(t2/insert! :model/RemoteSyncObject
|
|
(for [[model-type model-id coll-id] [["Collection" folder folder]
|
|
["Collection" sub-folder sub-folder]
|
|
["Card" card folder]
|
|
["Card" sub-card sub-folder]
|
|
["Card" sibling-card root]]]
|
|
{:model_type model-type :model_id model-id :model_collection_id coll-id
|
|
:model_name "x" :status "synced" :status_changed_at (t/offset-date-time)}))
|
|
(f {:root root :folder folder :sub-folder sub-folder :card card :sub-card sub-card :sibling-card sibling-card})))
|
|
|
|
(defn- set-subtree-archived!
|
|
"Sets `archived` on `folder` and its contents, as archiving and unarchiving a collection do in bulk SQL."
|
|
[{:keys [folder sub-folder card sub-card]} archived]
|
|
(t2/update! :model/Collection :id [:in [folder sub-folder]] {:archived archived})
|
|
(t2/update! :model/Card :id [:in [card sub-card]] {:archived archived}))
|
|
|
|
(deftest collection-update-event-archived-cascades-delete-to-subtree-test
|
|
(testing "GHY-4399: archiving a nested collection marks its descendant collections and all contents of its subtree
|
|
'delete', and leaves content outside the subtree alone"
|
|
(with-synced-nested-subtree!
|
|
(fn [{:keys [folder sub-folder card sub-card sibling-card] :as ids}]
|
|
(set-subtree-archived! ids true)
|
|
(events/publish-event! :event/collection-update
|
|
{:object (t2/select-one :model/Collection :id folder) :user-id (mt/user->id :rasta)})
|
|
(is (= {["Collection" folder] "delete"
|
|
["Collection" sub-folder] "delete"
|
|
["Card" card] "delete"
|
|
["Card" sub-card] "delete"
|
|
["Card" sibling-card] "synced"}
|
|
(rso-statuses)))))))
|
|
|
|
(deftest collection-update-event-unarchived-restores-subtree-test
|
|
(testing "GHY-4399: unarchiving a collection before the next push marks its unarchived contents 'update' again, so
|
|
the push does not delete their files; content still archived on its own stays 'delete'"
|
|
(with-synced-nested-subtree!
|
|
(fn [{:keys [folder sub-folder card sub-card sibling-card] :as ids}]
|
|
(set-subtree-archived! ids true)
|
|
(events/publish-event! :event/collection-update
|
|
{:object (t2/select-one :model/Collection :id folder) :user-id (mt/user->id :rasta)})
|
|
(set-subtree-archived! ids false)
|
|
(t2/update! :model/Card :id sub-card {:archived true})
|
|
(events/publish-event! :event/collection-update
|
|
{:object (t2/select-one :model/Collection :id folder) :user-id (mt/user->id :rasta)})
|
|
(is (= {["Collection" folder] "update"
|
|
["Collection" sub-folder] "update"
|
|
["Card" card] "update"
|
|
["Card" sub-card] "delete"
|
|
["Card" sibling-card] "synced"}
|
|
(rso-statuses)))))))
|
|
|
|
(deftest collection-update-event-archive-cascades-to-transforms-test
|
|
(testing "GHY-4399: archiving a Transforms folder marks the transforms in it 'delete', and unarchiving it marks them
|
|
'update' again; transforms have no archived column, so their folder's archived state decides"
|
|
(mt/with-temporary-setting-values [remote-sync-transforms true]
|
|
(mt/with-temp [:model/Collection {folder :id} {:name "Folder" :namespace "transforms" :location "/"}
|
|
:model/Transform {transform :id} {:name "Transform" :collection_id folder}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(t2/insert! :model/RemoteSyncObject
|
|
(for [[model-type model-id] [["Collection" folder] ["Transform" transform]]]
|
|
{:model_type model-type :model_id model-id :model_collection_id folder
|
|
:model_name "x" :status "synced" :status_changed_at (t/offset-date-time)}))
|
|
(let [set-archived! (fn [archived]
|
|
(t2/update! :model/Collection :id folder {:archived archived})
|
|
(events/publish-event! :event/collection-update
|
|
{:object (t2/select-one :model/Collection :id folder)
|
|
:user-id (mt/user->id :rasta)}))]
|
|
(set-archived! true)
|
|
(is (= {["Collection" folder] "delete"
|
|
["Transform" transform] "delete"}
|
|
(rso-statuses)))
|
|
(set-archived! false)
|
|
(is (= {["Collection" folder] "update"
|
|
["Transform" transform] "update"}
|
|
(rso-statuses))))))))
|
|
|
|
(deftest collection-update-event-no-entry-for-normal-collection-test
|
|
(testing "collection-update event doesn't create entry for non-remote-synced collections"
|
|
(mt/with-temp [:model/Collection normal-collection {:name "Normal"}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/collection-update
|
|
{:object normal-collection :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 0 (count entries)))))))
|
|
|
|
(deftest collection-multiple-update-events-update-same-object-test
|
|
(testing "multiple collection-update events update the same remote sync object"
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true
|
|
:name "Remote-Sync"}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(let [clock-t1 (t/mock-clock (t/instant "2024-01-01T10:00:00Z") (t/zone-id "UTC"))]
|
|
(t/with-clock clock-t1
|
|
(events/publish-event! :event/collection-update
|
|
{:object remote-sync-collection :user-id (mt/user->id :rasta)}))
|
|
(let [initial-entry (t2/select-one :model/RemoteSyncObject
|
|
:model_type "Collection"
|
|
:model_id (:id remote-sync-collection))
|
|
clock-t2 (t/mock-clock (t/instant "2024-01-01T11:00:00Z") (t/zone-id "UTC"))]
|
|
(t/with-clock clock-t2
|
|
(events/publish-event! :event/collection-update
|
|
{:object remote-sync-collection :user-id (mt/user->id :rasta)}))
|
|
(let [entries (t2/select :model/RemoteSyncObject :model_id (:id remote-sync-collection))]
|
|
(is (= 1 (count entries)))
|
|
(is (= (:id initial-entry) (:id (first entries))))
|
|
(is (t/after? (:status_changed_at (first entries))
|
|
(:status_changed_at initial-entry)))))))))
|
|
|
|
(deftest create-remote-sync-object-entry-creates-new-entry-test
|
|
(testing "create-remote-sync-object-entry! creates new entry when none exists"
|
|
(mt/with-temp [:model/Card card {:name "Test Card"
|
|
:dataset_query (mt/mbql-query venues)}]
|
|
(mt/with-current-user (mt/user->id :rasta)
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(let [hydrate-fn (fn [id] (t2/select-one [:model/Card :name :collection_id :display] :id id))]
|
|
(#'remote-sync.events/create-or-update-remote-sync-object-entry! "Card" (:id card) "create" hydrate-fn)
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Card"
|
|
:model_id (:id card)
|
|
:status "create"}
|
|
(first entries)))))))))
|
|
|
|
(deftest create-remote-sync-object-entry-updates-existing-entry-test
|
|
(testing "create-remote-sync-object-entry! updates existing entry when one exists"
|
|
(mt/with-temp [:model/Dashboard dashboard {:name "Test Dashboard"}]
|
|
(mt/with-current-user (mt/user->id :rasta)
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(let [clock-t1 (t/mock-clock (t/instant "2024-01-01T10:00:00Z") (t/zone-id "UTC"))
|
|
hydrate-fn (fn [id] (t2/select-one [:model/Dashboard :name :collection_id] :id id))]
|
|
(t/with-clock clock-t1
|
|
(#'remote-sync.events/create-or-update-remote-sync-object-entry! "Dashboard" (:id dashboard) "update" hydrate-fn))
|
|
(let [initial-entry (t2/select-one :model/RemoteSyncObject :model_type "Dashboard" :model_id (:id dashboard))
|
|
initial-time (:status_changed_at initial-entry)
|
|
clock-t2 (t/mock-clock (t/instant "2024-01-01T11:00:00Z") (t/zone-id "UTC"))]
|
|
(t/with-clock clock-t2
|
|
(#'remote-sync.events/create-or-update-remote-sync-object-entry! "Dashboard" (:id dashboard) "synced" hydrate-fn))
|
|
(let [entries (t2/select :model/RemoteSyncObject :model_type "Dashboard" :model_id (:id dashboard))]
|
|
(is (= 1 (count entries)))
|
|
(let [update-entry (first entries)]
|
|
(is (= (:id initial-entry) (:id update-entry)))
|
|
(is (= "synced" (:status update-entry)))
|
|
(is (t/after? (:status_changed_at update-entry) initial-time))))))))))
|
|
|
|
(deftest create-remote-sync-object-entry-does-not-update-create-status-test
|
|
(testing "create-remote-sync-object-entry! does not update entry when status is 'create'"
|
|
(mt/with-temp [:model/Dashboard dashboard {:name "Test Dashboard"}]
|
|
(mt/with-current-user (mt/user->id :rasta)
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(let [clock-t1 (t/mock-clock (t/instant "2024-01-01T10:00:00Z") (t/zone-id "UTC"))
|
|
hydrate-fn (fn [id] (t2/select-one [:model/Dashboard :name :collection_id] :id id))]
|
|
(t/with-clock clock-t1
|
|
(#'remote-sync.events/create-or-update-remote-sync-object-entry! "Dashboard" (:id dashboard) "create" hydrate-fn))
|
|
(let [initial-entry (t2/select-one :model/RemoteSyncObject :model_type "Dashboard" :model_id (:id dashboard))
|
|
clock-t2 (t/mock-clock (t/instant "2024-01-01T11:00:00Z") (t/zone-id "UTC"))]
|
|
(t/with-clock clock-t2
|
|
(#'remote-sync.events/create-or-update-remote-sync-object-entry! "Dashboard" (:id dashboard) "synced" hydrate-fn))
|
|
(let [entries (t2/select :model/RemoteSyncObject :model_type "Dashboard" :model_id (:id dashboard))]
|
|
(is (= 1 (count entries)))
|
|
(let [update-entry (first entries)]
|
|
(is (= (:id initial-entry) (:id update-entry)))
|
|
(is (= "create" (:status update-entry)))
|
|
(is (= (:status_changed_at initial-entry) (:status_changed_at update-entry)))))))))))
|
|
|
|
(deftest create-remote-sync-object-entry-with-hydrate-fn-test
|
|
(testing "create-remote-sync-object-entry! creates entry with hydrated details from hydrate-fn"
|
|
(mt/with-current-user (mt/user->id :rasta)
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(let [existing-collection-id (t2/select-one-fn :id [:model/Collection :id])
|
|
hydrate-fn (fn [id] (t2/select-one [:model/Collection :name [:id :collection_id]] :id id))]
|
|
(#'remote-sync.events/create-or-update-remote-sync-object-entry! "Collection" existing-collection-id "create" hydrate-fn)
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Collection"
|
|
:model_id existing-collection-id
|
|
:status "create"}
|
|
(first entries))))))))
|
|
|
|
(deftest existing-card-moved-out-of-remote-synced-collection-test
|
|
(testing "existing card moved out of remote-synced collection is marked as removed"
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Collection normal-collection {:name "Normal"}
|
|
:model/Card card {:name "Test Card"
|
|
:dataset_query (mt/mbql-query venues)
|
|
:collection_id (:id remote-sync-collection)}]
|
|
(mark-synced! {:by-entity-id {"Card" #{(:entity_id card)}}})
|
|
(let [initial-entry (t2/select-one :model/RemoteSyncObject :model_type "Card" :model_id (:id card))]
|
|
(is (= "synced" (:status initial-entry)))
|
|
(events/publish-event! :event/card-update
|
|
{:object (assoc card :collection_id (:id normal-collection))
|
|
:previous-object card
|
|
:user-id (mt/user->id :rasta)})
|
|
(let [update-entry (t2/select-one :model/RemoteSyncObject :model_type "Card" :model_id (:id card))]
|
|
(is (= "removed" (:status update-entry)))
|
|
(is (= (:id initial-entry) (:id update-entry))))))))
|
|
|
|
(deftest new-card-moved-out-of-remote-synced-collection-test
|
|
(testing "new card moved out of remote-synced collection is no longer tracked"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Collection normal-collection {:name "Normal"}
|
|
:model/Card card {:name "Test Card"
|
|
:dataset_query (mt/mbql-query venues)
|
|
:collection_id (:id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/card-create
|
|
{:object card :user-id (mt/user->id :rasta)})
|
|
(let [initial-entry (t2/select-one :model/RemoteSyncObject :model_type "Card" :model_id (:id card))]
|
|
(is (= "create" (:status initial-entry)))
|
|
(events/publish-event! :event/card-update
|
|
{:object (assoc card :collection_id (:id normal-collection))
|
|
:previous-object card
|
|
:user-id (mt/user->id :rasta)})
|
|
(let [update-entry (t2/select-one :model/RemoteSyncObject :model_type "Card" :model_id (:id card))]
|
|
(is (nil? update-entry))))))))
|
|
|
|
(deftest existing-dashboard-moved-out-of-remote-synced-collection-test
|
|
(testing "existing dashboard moved out of remote-synced collection is marked as removed"
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Collection normal-collection {:name "Normal"}
|
|
:model/Dashboard dashboard {:name "Test Dashboard"
|
|
:collection_id (:id remote-sync-collection)}]
|
|
(mark-synced! {:by-entity-id {"Dashboard" #{(:entity_id dashboard)}}})
|
|
(let [initial-entry (t2/select-one :model/RemoteSyncObject :model_type "Dashboard" :model_id (:id dashboard))]
|
|
(is (= "synced" (:status initial-entry)))
|
|
(events/publish-event! :event/dashboard-update
|
|
{:object (assoc dashboard :collection_id (:id normal-collection))
|
|
:user-id (mt/user->id :rasta)})
|
|
(let [update-entry (t2/select-one :model/RemoteSyncObject :model_type "Dashboard" :model_id (:id dashboard))]
|
|
(is (= "removed" (:status update-entry)))
|
|
(is (= (:id initial-entry) (:id update-entry))))))))
|
|
|
|
(deftest new-dashboard-moved-out-of-remote-synced-collection-test
|
|
(testing "new dashboard moved out of remote-synced collection is no longer tracked"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Collection normal-collection {:name "Normal"}
|
|
:model/Dashboard dashboard {:name "Test Dashboard"
|
|
:collection_id (:id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/dashboard-create
|
|
{:object dashboard :user-id (mt/user->id :rasta)})
|
|
(let [initial-entry (t2/select-one :model/RemoteSyncObject :model_type "Dashboard" :model_id (:id dashboard))]
|
|
(is (= "create" (:status initial-entry)))
|
|
(events/publish-event! :event/dashboard-update
|
|
{:object (assoc dashboard :collection_id (:id normal-collection))
|
|
:user-id (mt/user->id :rasta)})
|
|
(let [update-entry (t2/select-one :model/RemoteSyncObject :model_type "Dashboard" :model_id (:id dashboard))]
|
|
(is (nil? update-entry))))))))
|
|
|
|
(deftest existing-document-moved-out-of-remote-synced-collection-test
|
|
(testing "existing document moved out of remote-synced collection is marked as removed"
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Collection normal-collection {:name "Normal"}
|
|
:model/Document document {:collection_id (u/the-id remote-sync-collection)}]
|
|
(mark-synced! {:by-entity-id {"Document" #{(:entity_id document)}}})
|
|
(let [initial-entry (t2/select-one :model/RemoteSyncObject :model_type "Document" :model_id (:id document))]
|
|
(is (= "synced" (:status initial-entry)))
|
|
(events/publish-event! :event/document-update
|
|
{:object (assoc document :collection_id (:id normal-collection))
|
|
:user-id (mt/user->id :rasta)})
|
|
(let [update-entry (t2/select-one :model/RemoteSyncObject :model_type "Document" :model_id (:id document))]
|
|
(is (= "removed" (:status update-entry)))
|
|
(is (= (:id initial-entry) (:id update-entry))))))))
|
|
|
|
(deftest new-document-moved-out-of-remote-synced-collection-test
|
|
(testing "new document moved out of remote-synced collection is no longer tracked"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Collection normal-collection {:name "Normal"}
|
|
:model/Document document {:collection_id (u/the-id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/document-create
|
|
{:object document :user-id (mt/user->id :rasta)})
|
|
(let [initial-entry (t2/select-one :model/RemoteSyncObject :model_type "Document" :model_id (:id document))]
|
|
(is (= "create" (:status initial-entry)))
|
|
(events/publish-event! :event/document-update
|
|
{:object (assoc document :collection_id (:id normal-collection))
|
|
:user-id (mt/user->id :rasta)})
|
|
(let [update-entry (t2/select-one :model/RemoteSyncObject :model_type "Document" :model_id (:id document))]
|
|
(is (nil? update-entry))))))))
|
|
|
|
(deftest existing-collection-type-changed-from-remote-synced-test
|
|
(testing "existing collection type changed from remote-synced is marked as removed"
|
|
(mt/with-temp [:model/Collection collection {:is_remote_synced true :name "Remote-Sync"}]
|
|
(mark-synced! {:by-entity-id {"Collection" #{(:entity_id collection)}}})
|
|
(let [initial-entry (t2/select-one :model/RemoteSyncObject :model_type "Collection" :model_id (:id collection))]
|
|
(is (= "synced" (:status initial-entry)))
|
|
(events/publish-event! :event/collection-update
|
|
{:object (assoc collection :is_remote_synced false)
|
|
:user-id (mt/user->id :rasta)})
|
|
(let [update-entry (t2/select-one :model/RemoteSyncObject :model_type "Collection" :model_id (:id collection))]
|
|
(is (= "removed" (:status update-entry)))
|
|
(is (= (:id initial-entry) (:id update-entry))))))))
|
|
|
|
(deftest new-collection-type-changed-from-remote-synced-test
|
|
(testing "new collection type changed from remote-synced is marked as removed"
|
|
(mt/with-temp [:model/Collection collection {:is_remote_synced true :name "Remote-Sync"}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/collection-create
|
|
{:object collection :user-id (mt/user->id :rasta)})
|
|
(let [initial-entry (t2/select-one :model/RemoteSyncObject :model_type "Collection" :model_id (:id collection))]
|
|
(is (= "create" (:status initial-entry)))
|
|
(events/publish-event! :event/collection-update
|
|
{:object (assoc collection :is_remote_synced false)
|
|
:user-id (mt/user->id :rasta)})
|
|
(let [update-entry (t2/select-one :model/RemoteSyncObject :model_type "Collection" :model_id (:id collection))]
|
|
(is (nil? update-entry)))))))
|
|
|
|
(deftest model-not-tracked-moved-to-normal-collection-test
|
|
(testing "model not previously tracked doesn't create removed entry when moved to normal collection"
|
|
(mt/with-temp [:model/Collection normal-collection {:name "Normal"}
|
|
:model/Card card {:name "Test Card"
|
|
:dataset_query (mt/mbql-query venues)
|
|
:collection_id (:id normal-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/card-update
|
|
{:object card :previous-object card :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject :model_type "Card" :model_id (:id card))]
|
|
(is (= 0 (count entries)))))))
|
|
|
|
(deftest ^:parallel card-event-derivation-test
|
|
(testing "card events properly derive from :metabase/event"
|
|
(is (events/isa? ::remote-sync.events/card-change-event :metabase/event))
|
|
(is (events/isa? :event/card-create ::remote-sync.events/card-change-event))
|
|
(is (events/isa? :event/card-update ::remote-sync.events/card-change-event))
|
|
(is (events/isa? :event/card-delete ::remote-sync.events/card-change-event))))
|
|
|
|
(deftest ^:parallel dashboard-event-derivation-test
|
|
(testing "dashboard events properly derive from :metabase/event"
|
|
(is (events/isa? ::remote-sync.events/dashboard-change-event :metabase/event))
|
|
(is (events/isa? :event/dashboard-create ::remote-sync.events/dashboard-change-event))
|
|
(is (events/isa? :event/dashboard-update ::remote-sync.events/dashboard-change-event))
|
|
(is (events/isa? :event/dashboard-delete ::remote-sync.events/dashboard-change-event))))
|
|
|
|
(deftest ^:parallel document-event-derivation-test
|
|
(testing "document events properly derive from :metabase/event"
|
|
(is (events/isa? ::remote-sync.events/document-change-event :metabase/event))
|
|
(is (events/isa? :event/document-create ::remote-sync.events/document-change-event))
|
|
(is (events/isa? :event/document-update ::remote-sync.events/document-change-event))
|
|
(is (events/isa? :event/document-delete ::remote-sync.events/document-change-event))))
|
|
|
|
(deftest ^:parallel snippet-event-derivation-test
|
|
(testing "snippet events properly derive from :metabase/event"
|
|
(is (events/isa? ::remote-sync.events/snippet-change-event :metabase/event))
|
|
(is (events/isa? :event/snippet-create ::remote-sync.events/snippet-change-event))
|
|
(is (events/isa? :event/snippet-update ::remote-sync.events/snippet-change-event))
|
|
(is (events/isa? :event/snippet-delete ::remote-sync.events/snippet-change-event))))
|
|
|
|
(deftest ^:parallel collection-event-derivation-test
|
|
(testing "collection events properly derive from :metabase/event"
|
|
(is (events/isa? ::remote-sync.events/collection-change-event :metabase/event))
|
|
(is (events/isa? :event/collection-create ::remote-sync.events/collection-change-event))
|
|
(is (events/isa? :event/collection-update ::remote-sync.events/collection-change-event))))
|
|
|
|
(deftest timeline-create-event-creates-entry-test
|
|
(testing "timeline-create event creates remote sync object entry with create status"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Timeline timeline {:name "Test Timeline"
|
|
:collection_id (:id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/timeline-create
|
|
{:object timeline :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Timeline"
|
|
:model_id (:id timeline)
|
|
:status "create"}
|
|
(first entries))))))))
|
|
|
|
(deftest timeline-event-api-create-tracks-timeline-test
|
|
(testing "timeline-create event with a Timeline object creates a remote sync entry (fixed behavior)"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Timeline timeline {:name "Test Timeline"
|
|
:collection_id (:id remote-sync-collection)}
|
|
:model/TimelineEvent timeline-event {:name "Test Event"
|
|
:timestamp (t/offset-date-time)
|
|
:timezone "UTC"
|
|
:time_matters false
|
|
:timeline_id (:id timeline)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(testing "publishing with a TimelineEvent (old buggy behavior) does NOT create a sync entry"
|
|
(events/publish-event! :event/timeline-create
|
|
{:object timeline-event :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 0 (count entries))
|
|
"TimelineEvent lacks collection_id, so no sync entry should be created")))
|
|
(testing "publishing with a Timeline (correct behavior) creates a sync entry"
|
|
(events/publish-event! :event/timeline-create
|
|
{:object timeline :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries))
|
|
"Should create exactly one remote sync entry")
|
|
(is (=? {:model_type "Timeline"
|
|
:model_id (:id timeline)
|
|
:status "create"}
|
|
(first entries))
|
|
"Remote sync entry should track the Timeline, not the TimelineEvent")))))))
|
|
|
|
(deftest timeline-update-event-creates-entry-test
|
|
(testing "timeline-update event creates remote sync object entry with update status"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Timeline timeline {:name "Test Timeline"
|
|
:collection_id (:id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/timeline-update
|
|
{:object timeline :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Timeline"
|
|
:model_id (:id timeline)
|
|
:status "update"}
|
|
(first entries))))))))
|
|
|
|
(deftest timeline-update-archived-sets-delete-test
|
|
(testing "timeline-update event with archived timeline sets delete status"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Timeline timeline {:name "Test Timeline"
|
|
:collection_id (:id remote-sync-collection)
|
|
:archived true}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/timeline-update
|
|
{:object timeline :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Timeline"
|
|
:model_id (:id timeline)
|
|
:status "delete"}
|
|
(first entries))))))))
|
|
|
|
(deftest timeline-delete-event-creates-entry-test
|
|
(testing "timeline-delete event creates remote sync object entry with delete status"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Timeline timeline {:name "Test Timeline"
|
|
:collection_id (:id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/timeline-delete
|
|
{:object timeline :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Timeline"
|
|
:model_id (:id timeline)
|
|
:status "delete"}
|
|
(first entries))))))))
|
|
|
|
(deftest timeline-event-in-normal-collection-no-entry-test
|
|
(testing "timeline events in normal collections don't create remote sync entries"
|
|
(mt/with-temp [:model/Collection normal-collection {:name "Normal Collection"}
|
|
:model/Timeline timeline {:name "Test Timeline"
|
|
:collection_id (:id normal-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/timeline-create
|
|
{:object timeline :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 0 (count entries)))))))
|
|
|
|
(deftest existing-timeline-moved-out-of-remote-synced-collection-test
|
|
(testing "existing timeline moved out of remote-synced collection gets marked as removed"
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Collection normal-collection {:name "Normal Collection"}
|
|
:model/Timeline timeline {:name "Test Timeline"
|
|
:collection_id (:id remote-sync-collection)}]
|
|
(mark-synced! {:by-entity-id {"Timeline" #{(:entity_id timeline)}}})
|
|
(is (= 1 (count (t2/select :model/RemoteSyncObject))))
|
|
(events/publish-event! :event/timeline-update
|
|
{:object (assoc timeline :collection_id (:id normal-collection))
|
|
:user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Timeline"
|
|
:model_id (:id timeline)
|
|
:status "removed"}
|
|
(first entries)))))))
|
|
|
|
(deftest new-timeline-moved-out-of-remote-synced-collection-test
|
|
(testing "new timeline moved out of remote-synced collection is no longer tracked"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Collection normal-collection {:name "Normal Collection"}
|
|
:model/Timeline timeline {:name "Test Timeline"
|
|
:collection_id (:id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/timeline-create
|
|
{:object timeline :user-id (mt/user->id :rasta)})
|
|
(is (= 1 (count (t2/select :model/RemoteSyncObject))))
|
|
(events/publish-event! :event/timeline-update
|
|
{:object (assoc timeline :collection_id (:id normal-collection))
|
|
:user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 0 (count entries))))))))
|
|
|
|
(deftest soft-delete-then-hard-delete-preserves-dirty-status-test
|
|
(testing "doc that is soft-deleted then hard-deleted should maintain dirty status"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Document doc {:name "Test Doc"
|
|
:collection_id (:id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
;; Trash the doc
|
|
(t2/update! :model/Document (:id doc) {:archived true})
|
|
(events/publish-event! :event/document-update
|
|
{:object (assoc doc :archived true)
|
|
:user-id (mt/user->id :rasta)})
|
|
(let [soft-delete-entry (t2/select-one :model/RemoteSyncObject :model_type "Document" :model_id (:id doc))]
|
|
(is (= "delete" (:status soft-delete-entry))))
|
|
;; Permanently delete from trash
|
|
(t2/delete! :model/Document (:id doc))
|
|
(events/publish-event! :event/document-delete
|
|
{:object doc :user-id (mt/user->id :rasta)})
|
|
;; The remote sync object entry should still exist with delete status
|
|
;; so the collection remains dirty
|
|
(let [hard-delete-entry (t2/select-one :model/RemoteSyncObject :model_type "Document" :model_id (:id doc))]
|
|
(is (= "delete" (:status hard-delete-entry)))
|
|
(is (not (nil? hard-delete-entry))))))))
|
|
|
|
;;; Table Event Tests
|
|
|
|
(deftest table-update-event-creates-entry-for-published-table-test
|
|
(testing "table-update event creates remote sync object entry for published table in remote-synced collection"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Table table {:name "Test Table"
|
|
:is_published true
|
|
:collection_id (:id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/table-update
|
|
{:object table :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Table"
|
|
:model_id (:id table)
|
|
:status "update"}
|
|
(first entries))))))))
|
|
|
|
(deftest table-update-event-no-entry-for-unpublished-table-test
|
|
(testing "table-update event doesn't create entry for unpublished table"
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Table table {:name "Test Table"
|
|
:is_published false
|
|
:collection_id (:id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/table-update
|
|
{:object table :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 0 (count entries)))))))
|
|
|
|
(deftest table-update-event-no-entry-for-normal-collection-test
|
|
(testing "table-update event doesn't create entry for table in non-remote-synced collection"
|
|
(mt/with-temp [:model/Collection normal-collection {:name "Normal"}
|
|
:model/Table table {:name "Test Table"
|
|
:is_published true
|
|
:collection_id (:id normal-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/table-update
|
|
{:object table :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 0 (count entries)))))))
|
|
|
|
(deftest table-update-archived-sets-delete-test
|
|
(testing "table-update event with archived_at sets delete status"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Table table {:name "Test Table"
|
|
:is_published true
|
|
:collection_id (:id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/table-update
|
|
{:object (assoc table :archived_at (t/offset-date-time))
|
|
:user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Table"
|
|
:model_id (:id table)
|
|
:status "delete"}
|
|
(first entries))))))))
|
|
|
|
(deftest table-unpublished-marks-as-removed-test
|
|
(testing "table that becomes unpublished is marked as removed"
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Table table {:name "Test Table"
|
|
:is_published true
|
|
:collection_id (:id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
;; First, create an entry with status "synced" directly
|
|
(t2/insert! :model/RemoteSyncObject {:model_type "Table"
|
|
:model_id (:id table)
|
|
:model_name "Test Table"
|
|
:model_table_id (:id table)
|
|
:model_table_name "Test Table"
|
|
:status "synced"
|
|
:status_changed_at (t/offset-date-time)})
|
|
(let [initial-entry (t2/select-one :model/RemoteSyncObject :model_type "Table" :model_id (:id table))]
|
|
(is (= "synced" (:status initial-entry)))
|
|
;; Now unpublish the table
|
|
(events/publish-event! :event/table-update
|
|
{:object (assoc table :is_published false)
|
|
:user-id (mt/user->id :rasta)})
|
|
(let [update-entry (t2/select-one :model/RemoteSyncObject :model_type "Table" :model_id (:id table))]
|
|
(is (= "removed" (:status update-entry))))))))
|
|
|
|
(deftest table-moved-to-normal-collection-marks-as-removed-test
|
|
(testing "table moved to non-remote-synced collection is marked as removed"
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Collection normal-collection {:name "Normal"}
|
|
:model/Table table {:name "Test Table"
|
|
:is_published true
|
|
:collection_id (:id remote-sync-collection)}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
;; First, create an entry with status "synced" directly
|
|
(t2/insert! :model/RemoteSyncObject {:model_type "Table"
|
|
:model_id (:id table)
|
|
:model_name "Test Table"
|
|
:model_table_id (:id table)
|
|
:model_table_name "Test Table"
|
|
:status "synced"
|
|
:status_changed_at (t/offset-date-time)})
|
|
(let [initial-entry (t2/select-one :model/RemoteSyncObject :model_type "Table" :model_id (:id table))]
|
|
(is (= "synced" (:status initial-entry)))
|
|
;; Now move to normal collection
|
|
(events/publish-event! :event/table-update
|
|
{:object (assoc table :collection_id (:id normal-collection))
|
|
:user-id (mt/user->id :rasta)})
|
|
(let [update-entry (t2/select-one :model/RemoteSyncObject :model_type "Table" :model_id (:id table))]
|
|
(is (= "removed" (:status update-entry))))))))
|
|
|
|
(deftest ^:parallel table-event-derivation-test
|
|
(testing "table events properly derive from :metabase/event"
|
|
(is (events/isa? ::remote-sync.events/table-change-event :metabase/event))
|
|
(is (events/isa? :event/table-create ::remote-sync.events/table-change-event))
|
|
(is (events/isa? :event/table-update ::remote-sync.events/table-change-event))
|
|
(is (events/isa? :event/table-delete ::remote-sync.events/table-change-event))))
|
|
|
|
;;; Segment Event Tests
|
|
|
|
(deftest segment-create-event-creates-entry-test
|
|
(testing "segment-create event creates remote sync object entry for segment in published table in remote-synced collection"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Table table {:name "Test Table"
|
|
:is_published true
|
|
:collection_id (:id remote-sync-collection)}
|
|
:model/Segment segment {:name "Test Segment"
|
|
:table_id (:id table)
|
|
:definition {:source-table (:id table)
|
|
:filter [:> [:field 1 nil] 0]}}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/segment-create
|
|
{:object segment :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Segment"
|
|
:model_id (:id segment)
|
|
:status "create"}
|
|
(first entries))))))))
|
|
|
|
(deftest segment-update-event-creates-entry-test
|
|
(testing "segment-update event creates remote sync object entry with update status"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Table table {:name "Test Table"
|
|
:is_published true
|
|
:collection_id (:id remote-sync-collection)}
|
|
:model/Segment segment {:name "Test Segment"
|
|
:table_id (:id table)
|
|
:definition {:source-table (:id table)
|
|
:filter [:> [:field 1 nil] 0]}}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/segment-update
|
|
{:object segment :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Segment"
|
|
:model_id (:id segment)
|
|
:status "update"}
|
|
(first entries))))))))
|
|
|
|
(deftest segment-update-archived-sets-delete-test
|
|
(testing "segment-update event with archived=true sets delete status"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Table table {:name "Test Table"
|
|
:is_published true
|
|
:collection_id (:id remote-sync-collection)}
|
|
:model/Segment segment {:name "Test Segment"
|
|
:table_id (:id table)
|
|
:definition {:source-table (:id table)
|
|
:filter [:> [:field 1 nil] 0]}}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/segment-update
|
|
{:object (assoc segment :archived true)
|
|
:user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Segment"
|
|
:model_id (:id segment)
|
|
:status "delete"}
|
|
(first entries))))))))
|
|
|
|
(deftest segment-delete-event-creates-entry-test
|
|
(testing "segment-delete event creates remote sync object entry with delete status"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Table table {:name "Test Table"
|
|
:is_published true
|
|
:collection_id (:id remote-sync-collection)}
|
|
:model/Segment segment {:name "Test Segment"
|
|
:table_id (:id table)
|
|
:definition {:source-table (:id table)
|
|
:filter [:> [:field 1 nil] 0]}}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/segment-delete
|
|
{:object segment :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)))
|
|
(is (=? {:model_type "Segment"
|
|
:model_id (:id segment)
|
|
:status "delete"}
|
|
(first entries))))))))
|
|
|
|
(deftest segment-event-in-normal-collection-no-entry-test
|
|
(testing "segment events in non-remote-synced collections don't create entries"
|
|
(mt/with-temp [:model/Collection normal-collection {:name "Normal"}
|
|
:model/Table table {:name "Test Table"
|
|
:is_published true
|
|
:collection_id (:id normal-collection)}
|
|
:model/Segment segment {:name "Test Segment"
|
|
:table_id (:id table)
|
|
:definition {:source-table (:id table)
|
|
:filter [:> [:field 1 nil] 0]}}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/segment-create
|
|
{:object segment :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 0 (count entries)))))))
|
|
|
|
(deftest segment-event-unpublished-table-no-entry-test
|
|
(testing "segment events for unpublished tables don't create entries"
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Table table {:name "Test Table"
|
|
:is_published false
|
|
:collection_id (:id remote-sync-collection)}
|
|
:model/Segment segment {:name "Test Segment"
|
|
:table_id (:id table)
|
|
:definition {:source-table (:id table)
|
|
:filter [:> [:field 1 nil] 0]}}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/segment-create
|
|
{:object segment :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 0 (count entries)))))))
|
|
|
|
(deftest existing-segment-table-unpublished-marks-as-removed-test
|
|
(testing "existing segment is marked as removed when its table is unpublished"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync"}
|
|
:model/Table table {:name "Test Table"
|
|
:is_published true
|
|
:collection_id (:id remote-sync-collection)}
|
|
:model/Segment segment {:name "Test Segment"
|
|
:table_id (:id table)
|
|
:definition {:source-table (:id table)
|
|
:filter [:> [:field 1 nil] 0]}}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
;; First create a synced entry
|
|
(t2/insert! :model/RemoteSyncObject {:model_type "Segment"
|
|
:model_id (:id segment)
|
|
:model_name "Test Segment"
|
|
:model_table_id (:id table)
|
|
:model_table_name "Test Table"
|
|
:status "synced"
|
|
:status_changed_at (t/offset-date-time)})
|
|
(let [initial-entry (t2/select-one :model/RemoteSyncObject :model_type "Segment" :model_id (:id segment))]
|
|
(is (= "synced" (:status initial-entry)))
|
|
;; Now "unpublish" the table by simulating an update where the segment's table is no longer in sync scope
|
|
(events/publish-event! :event/segment-update
|
|
{:object (assoc segment :table_id (:id table))
|
|
:user-id (mt/user->id :rasta)})
|
|
;; Since table is still published, should be "update"
|
|
(let [update-entry (t2/select-one :model/RemoteSyncObject :model_type "Segment" :model_id (:id segment))]
|
|
(is (= "update" (:status update-entry)))))))))
|
|
|
|
(deftest ^:parallel segment-event-derivation-test
|
|
(testing "segment events properly derive from :metabase/event"
|
|
(is (events/isa? ::remote-sync.events/segment-change-event :metabase/event))
|
|
(is (events/isa? :event/segment-create ::remote-sync.events/segment-change-event))
|
|
(is (events/isa? :event/segment-update ::remote-sync.events/segment-change-event))
|
|
(is (events/isa? :event/segment-delete ::remote-sync.events/segment-change-event))))
|
|
|
|
;;; Field Event Tests
|
|
|
|
(deftest field-update-event-no-entry-for-unpublished-table-test
|
|
(testing "field-update event creates no RSOs for an unpublished table"
|
|
(mt/with-temp [:model/Collection remote-sync-collection {:is_remote_synced true :name "Remote-Sync" :type "library-data"}
|
|
:model/Table table {:name "Test Table"
|
|
:is_published false
|
|
:collection_id (:id remote-sync-collection)}
|
|
:model/Field field {:name "test_field"
|
|
:table_id (:id table)
|
|
:base_type :type/Text}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/field-update
|
|
{:object field :user-id (mt/user->id :rasta)})
|
|
(is (empty? (t2/select :model/RemoteSyncObject))))))
|
|
|
|
(deftest field-update-event-no-entry-for-normal-collection-test
|
|
(testing "field-update event creates no RSOs for a table in a non-remote-synced collection"
|
|
(mt/with-temp [:model/Collection normal-collection {:name "Normal" :type "library-data"}
|
|
:model/Table table {:name "Test Table"
|
|
:is_published true
|
|
:collection_id (:id normal-collection)}
|
|
:model/Field field {:name "test_field"
|
|
:table_id (:id table)
|
|
:base_type :type/Text}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/field-update
|
|
{:object field :user-id (mt/user->id :rasta)})
|
|
(is (empty? (t2/select :model/RemoteSyncObject))))))
|
|
|
|
;;; ----------------------------------------- TableUserSettings RSO Tracking from Field Events --------------------------
|
|
|
|
(deftest field-update-creates-field-user-settings-rso-when-fus-exists-test
|
|
(testing "field-update on an eligible field with a FUS row creates a FieldUserSettings RSO for the Field — no
|
|
Field or TableUserSettings RSO"
|
|
(mt/with-temp [:model/Collection coll {:is_remote_synced true :name "Remote-Sync" :type "library-data"}
|
|
:model/Table table {:name "T" :is_published true :collection_id (:id coll)}
|
|
:model/Field field {:name "f" :table_id (:id table) :base_type :type/Text}]
|
|
(t2/insert! :model/FieldUserSettings {:field_id (:id field) :description "curated"})
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/field-update {:object field :user-id (mt/user->id :rasta)})
|
|
(let [entries (t2/select :model/RemoteSyncObject)]
|
|
(is (= 1 (count entries)) "exactly one RSO — the Field's FieldUserSettings")
|
|
(is (=? {:model_type "FieldUserSettings"
|
|
:model_id (:id field)
|
|
:status "update"
|
|
:model_table_id (:id table)}
|
|
(first entries)))))))
|
|
|
|
(deftest field-update-tracks-field-of-table-published-in-its-user-settings-test
|
|
(testing "field-update on a field of a Table published through its TableUserSettings creates a FieldUserSettings RSO"
|
|
(mt/with-temp [:model/Collection coll {:is_remote_synced true :name "Remote-Sync" :type "library-data"}
|
|
:model/Table table {:name "T"}
|
|
:model/Field field {:name "f" :table_id (:id table) :base_type :type/Text}]
|
|
(t2/insert! :model/TableUserSettings {:table_id (:id table) :is_published true :collection_id (:id coll)})
|
|
(t2/insert! :model/FieldUserSettings {:field_id (:id field) :description "curated"})
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/field-update {:object field :user-id (mt/user->id :rasta)})
|
|
(is (=? [{:model_type "FieldUserSettings" :model_id (:id field) :status "update"}]
|
|
(t2/select :model/RemoteSyncObject))))))
|
|
|
|
;;; ------------------------------------- Concurrent Un-Sync Race Tests -------------------------------------
|
|
;;;
|
|
;;; When a card-update event and a collection disable run concurrently, the handler must not resurrect the
|
|
;;; pending 'removed' status the disable records. Two orderings, each guarded by a different mechanism:
|
|
;;; - disable commits first -> the handler's write-time eligibility re-check keeps the removal
|
|
;;; - handler locks first -> the disable blocks on the row lock and records the removal last
|
|
;;; Both are exercised below by parking the handler at a chosen point and driving the disable around it.
|
|
|
|
(defn- once-parking-select-one!
|
|
"Wraps `t2/select-one` to park the first call whose args match `park-pred` — signalling `reached`, then
|
|
blocking on `release` (10s cap) — so a test can freeze the handler at a precise point."
|
|
[park-pred reached release]
|
|
(let [orig t2/select-one
|
|
fired (atom false)]
|
|
(fn [& args]
|
|
(let [res (apply orig args)]
|
|
(when (and (park-pred args) (compare-and-set! fired false true))
|
|
(deliver reached true)
|
|
(deref release 10000 nil))
|
|
res))))
|
|
|
|
(defn- rso-lookup-select?
|
|
"The handler's plain (non-locking) existing-entry read: `(t2/select-one :model/RemoteSyncObject :model_type … :model_id …)`."
|
|
[[model second-arg]]
|
|
(and (= model :model/RemoteSyncObject) (keyword? second-arg)))
|
|
|
|
(defn- rso-locking-select?
|
|
"The `FOR UPDATE` read inside create-or-update: `(t2/select-one :model/RemoteSyncObject {:where … :for :update})`."
|
|
[[model second-arg]]
|
|
(and (= model :model/RemoteSyncObject) (map? second-arg) (= :update (:for second-arg))))
|
|
|
|
(defn- do-with-unsync-race-fixture
|
|
"Sets up a remote-synced collection + card already on the remote (status 'synced', as after an export;
|
|
no content_hash, so the handler's no-op suppression stays out of the way) and calls `(f coll-id card)`.
|
|
Rows are committed because the event handler runs on another thread with its own connection."
|
|
[f]
|
|
(binding [tu.thread-local/*thread-local* false]
|
|
(mt/with-temp [:model/Collection {coll-id :id} {:name "Race-Sync" :location "/" :is_remote_synced true}
|
|
:model/Card card {:name "Race Card" :collection_id coll-id
|
|
:database_id (mt/id)
|
|
:dataset_query (mt/mbql-query venues)}]
|
|
(try
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(t2/insert! :model/RemoteSyncObject
|
|
[{:model_type "Collection" :model_id coll-id :model_name "Race-Sync"
|
|
:status "synced" :status_changed_at (t/offset-date-time)
|
|
:file_path "collections/rs/rs.yaml"}
|
|
{:model_type "Card" :model_id (:id card) :model_name "Race Card"
|
|
:model_collection_id coll-id :status "synced"
|
|
:status_changed_at (t/offset-date-time)
|
|
:file_path "collections/rs/cards/race.yaml"}])
|
|
(f coll-id card)
|
|
(finally
|
|
(t2/delete! :model/RemoteSyncObject))))))
|
|
|
|
(defn- fire-card-update! [card]
|
|
(future (events/publish-event! :event/card-update
|
|
{:object card :previous-object card :user-id (mt/user->id :rasta)})))
|
|
|
|
(defn- assert-removal-survived [coll-id card]
|
|
(is (= "removed" (t2/select-one-fn :status :model/RemoteSyncObject :model_type "Card" :model_id (:id card)))
|
|
"the card's pending removal survives the concurrent event")
|
|
(is (= "removed" (t2/select-one-fn :status :model/RemoteSyncObject :model_type "Collection" :model_id coll-id))
|
|
"the collection's pending removal is untouched"))
|
|
|
|
(deftest disable-first-recheck-preserves-removal-test
|
|
(testing "disable commits between the handler's stale eligibility read and its write: the write-time
|
|
eligibility re-check must keep the pending removal (GHY-4189)"
|
|
(do-with-unsync-race-fixture
|
|
(fn [coll-id card]
|
|
(let [reached (promise)
|
|
release (promise)]
|
|
;; Park at the handler's plain existing-entry read — before it takes any row lock. Nothing is
|
|
;; locked while parked, so the disable runs to completion first (the disable-first ordering).
|
|
(with-redefs [t2/select-one (once-parking-select-one! rso-lookup-select? reached release)]
|
|
(let [handler (fire-card-update! card)]
|
|
(is (true? (deref reached 10000 false)) "handler parked after reading pre-disable state")
|
|
(remote-sync.core/bulk-set-remote-sync {coll-id false})
|
|
(deliver release true)
|
|
(deref handler 10000 nil))))
|
|
(assert-removal-survived coll-id card)))))
|
|
|
|
(deftest handler-first-lock-preserves-removal-test
|
|
(testing "the handler holds the entry's row lock when the disable arrives: the disable blocks until the
|
|
handler commits, then records the removal last, so it survives (GHY-4189)"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(do-with-unsync-race-fixture
|
|
(fn [coll-id card]
|
|
(let [reached (promise)
|
|
release (promise)]
|
|
;; Park at the FOR UPDATE read, i.e. while the handler holds the entry's row lock (the
|
|
;; handler-first ordering).
|
|
(with-redefs [t2/select-one (once-parking-select-one! rso-locking-select? reached release)]
|
|
(let [handler (fire-card-update! card)]
|
|
(is (true? (deref reached 10000 false)) "handler parked holding the row lock")
|
|
(let [disable (future (remote-sync.core/bulk-set-remote-sync {coll-id false}))]
|
|
(is (= ::blocked (deref disable 300 ::blocked))
|
|
"the disable blocks on the row lock the handler holds")
|
|
(deliver release true)
|
|
(deref disable 10000 nil)
|
|
(deref handler 10000 nil)))))
|
|
(assert-removal-survived coll-id card))))))
|
|
|
|
(deftest dimension-change-creates-dimension-rso-test
|
|
(testing "adding or removing a Field's Dimension through the API tracks the Field's Dimension"
|
|
(mt/with-temporary-setting-values [remote-sync-type :read-write]
|
|
(mt/with-temp [:model/Collection coll {:is_remote_synced true :name "Remote-Sync" :type "library-data"}
|
|
:model/Table table {:name "T" :is_published true :collection_id (:id coll)}
|
|
:model/Field field {:name "f" :table_id (:id table) :base_type :type/Text}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(mt/user-http-request :crowberto :post 200 (format "field/%d/dimension" (:id field))
|
|
{:type "internal" :name "Remapped"})
|
|
(is (=? [{:model_type "Dimension" :model_id (:id field) :status "update"}]
|
|
(t2/select :model/RemoteSyncObject)))
|
|
(mt/user-http-request :crowberto :delete 204 (format "field/%d/dimension" (:id field)))
|
|
(is (=? [{:model_type "Dimension" :model_id (:id field) :status "removed"}]
|
|
(t2/select :model/RemoteSyncObject)))))))
|
|
|
|
(deftest field-update-no-rso-when-no-fus-row-test
|
|
(testing "field-update on an eligible field with NO FUS row, and no TableUserSettings row either, creates no
|
|
RSOs at all"
|
|
(mt/with-temp [:model/Collection coll {:is_remote_synced true :name "Remote-Sync" :type "library-data"}
|
|
:model/Table table {:name "T" :is_published true :collection_id (:id coll)}
|
|
:model/Field field {:name "f" :table_id (:id table) :base_type :type/Text}]
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/field-update {:object field :user-id (mt/user->id :rasta)})
|
|
(is (empty? (t2/select :model/RemoteSyncObject))
|
|
"no RSOs created — neither the Field nor the Table has user-curated metadata to track"))))
|
|
|
|
(deftest field-update-no-rso-for-unpublished-table-test
|
|
(testing "field-update creates no RSOs when the table is not published"
|
|
(mt/with-temp [:model/Collection coll {:is_remote_synced true :name "Remote-Sync" :type "library-data"}
|
|
:model/Table table {:name "T" :is_published false :collection_id (:id coll)}
|
|
:model/Field field {:name "f" :table_id (:id table) :base_type :type/Text}]
|
|
(t2/insert! :model/FieldUserSettings {:field_id (:id field) :description "curated"})
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/field-update {:object field :user-id (mt/user->id :rasta)})
|
|
(is (empty? (t2/select :model/RemoteSyncObject))))))
|
|
|
|
(deftest field-update-no-rso-for-non-synced-collection-test
|
|
(testing "field-update creates no RSOs when the collection is not remote-synced"
|
|
(mt/with-temp [:model/Collection coll {:name "Normal" :type "library-data"}
|
|
:model/Table table {:name "T" :is_published true :collection_id (:id coll)}
|
|
:model/Field field {:name "f" :table_id (:id table) :base_type :type/Text}]
|
|
(t2/insert! :model/FieldUserSettings {:field_id (:id field) :description "curated"})
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(events/publish-event! :event/field-update {:object field :user-id (mt/user->id :rasta)})
|
|
(is (empty? (t2/select :model/RemoteSyncObject))))))
|
|
|
|
(deftest field-update-field-user-settings-rso-marks-removed-when-table-leaves-sync-scope-test
|
|
(testing "an existing FieldUserSettings RSO is marked removed when the field's table leaves sync scope"
|
|
(mt/with-temp [:model/Collection synced {:is_remote_synced true :name "Synced" :type "library-data"}
|
|
:model/Collection normal {:name "Normal" :type "library-data"}
|
|
:model/Table table {:name "T" :is_published true :collection_id (:id synced)}
|
|
:model/Field field {:name "f" :table_id (:id table) :base_type :type/Text}]
|
|
(t2/insert! :model/FieldUserSettings {:field_id (:id field) :description "curated"})
|
|
(t2/delete! :model/RemoteSyncObject)
|
|
(t2/insert! :model/RemoteSyncObject {:model_type "FieldUserSettings"
|
|
:model_id (:id field)
|
|
:model_name "f"
|
|
:model_table_id (:id table)
|
|
:status "synced"
|
|
:status_changed_at (t/offset-date-time)})
|
|
(t2/update! :model/Table (:id table) {:collection_id (:id normal)})
|
|
(events/publish-event! :event/field-update {:object field :user-id (mt/user->id :rasta)})
|
|
(is (=? [{:model_type "FieldUserSettings" :model_id (:id field) :status "removed"}]
|
|
(t2/select :model/RemoteSyncObject))))))
|