diff --git a/CLAUDE.md b/CLAUDE.md index a87ca81..f39dd1b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -88,6 +88,7 @@ mirthSync is a Clojure-based command-line tool for synchronizing Mirth Connect c ### Debugging and Troubleshooting Best Practices - **Integration Tests for API Issues**: When troubleshooting API-related problems, prefer integration tests that spin up real Mirth instances over unit tests. Integration tests catch actual API behavior that unit tests may miss. - **API Investigation Process**: Always check OpenAPI/Swagger specifications when API calls aren't working as expected. Compare what the code sends vs what the API documentation requires - query parameters and request structure are often critical. +- **API Documentation Access**: When the Mirth server (oieserver) is running, the OpenAPI specification is available at `https://localhost:8443/api/openapi.json` for reviewing API endpoints and request/response formats. - **Mirth Configuration Map Structure**: Configuration map entries require specific XML structure with ConfigurationProperty objects containing `` and `` elements. Simple string values will not work. - **Server Process Management**: Integration tests require clean server state. Kill any manually running Mirth servers (mcserver/oieserver) before running the full test suite to avoid conflicts. - **CLI Flag Consistency**: Ensure CLI arguments are respected across all disk modes. For example, backup mode should still honor user preferences like `--include-configuration-map`. diff --git a/src/mirthsync/apis.clj b/src/mirthsync/apis.clj index 134dcf4..caa14ee 100644 --- a/src/mirthsync/apis.clj +++ b/src/mirthsync/apis.clj @@ -219,7 +219,8 @@ (defmethod mi/after-push :channels [api app-conf result] (if (true-200 result) (do - (when (:deploy app-conf) + (cond + (:deploy app-conf) (try+ (mhttp/post-xml app-conf @@ -229,7 +230,12 @@ false) (catch Object {:keys [body]} (log/warn (str "There was an error deploying the channel. -" body))))) +" body)))) + + (:deploy-all app-conf) + (let [channel-id (mi/find-id api (:el-loc app-conf))] + (log/infof "Collecting channel ID for bulk deployment: %s" channel-id) + (swap! (:bulk-deploy-channels app-conf) conj channel-id))) true) (do (log/error (str "Unable to save the channel." (when-not (app-conf :force) " There may be remote changes or the remote version does not match the local version. If you want to push the local changes anyway you can use the \"-f\" flag to force an overwrite."))) @@ -239,6 +245,27 @@ (defmethod mi/after-push :alerts [_ app-conf result] (null-204 result)) (defmethod mi/after-push :server-configuration [_ app-conf result] (null-204 result)) +(defn deploy-all-channels + "Deploy all collected channels in one API call." + [app-conf] + (when-let [channel-ids @(:bulk-deploy-channels app-conf)] + (when (seq channel-ids) + (log/infof "Deploying %d channels in bulk: %s" (count channel-ids) (pr-str channel-ids)) + (try+ + (let [channel-set (apply str "" + (map #(str "" % "") channel-ids) + "")] + (mhttp/post-xml + app-conf + "/channels/_deploy" + channel-set + {:returnErrors "true" :debug "false"} + false) + (log/info "Bulk channel deployment completed successfully")) + (catch Object {:keys [body]} + (log/error (str "Error during bulk channel deployment: " body)) + false))))) + (defmethod mi/pre-node-action :default [_ app-conf] app-conf) (defmethod mi/pre-node-action :code-template-libraries [_ app-conf] (pre-node-action :server-codelibs app-conf)) diff --git a/src/mirthsync/cli.clj b/src/mirthsync/cli.clj index b104171..3852260 100644 --- a/src/mirthsync/cli.clj +++ b/src/mirthsync/cli.clj @@ -84,10 +84,14 @@ disabled channels should be pushed or pulled. Default: false" :default false] - ["-d" "--deploy" "Deply channels on push + ["-d" "--deploy" "Deploy channels on push During a push, deploy each included channel immediately after saving the channel to Mirth."] + [nil "--deploy-all" "Deploy all channels in one API call at the end + During a push, collect all channel IDs and deploy them together + at the end, allowing Mirth's dependency logic to control order."] + ["-I" "--interactive" " Allow for console prompts for user input"] diff --git a/src/mirthsync/core.clj b/src/mirthsync/core.clj index 8e75102..58a6b02 100644 --- a/src/mirthsync/core.clj +++ b/src/mirthsync/core.clj @@ -38,7 +38,14 @@ conf-with-pre-pull (if (= "pull" action) (api/iterate-apis preprocessed-conf (api/apis preprocessed-conf) act/capture-pre-pull-local-files) preprocessed-conf) - processed-conf (api/iterate-apis conf-with-pre-pull (api/apis conf-with-pre-pull) action-fn)] + ;; Initialize bulk deployment atom if needed + conf-with-bulk-deploy (if (and (= "push" action) (:deploy-all conf-with-pre-pull)) + (assoc conf-with-pre-pull :bulk-deploy-channels (atom [])) + conf-with-pre-pull) + processed-conf (api/iterate-apis conf-with-bulk-deploy (api/apis conf-with-bulk-deploy) action-fn)] + ;; After push with --deploy-all, deploy all channels + (when (and (= "push" action) (:deploy-all processed-conf)) + (api/deploy-all-channels processed-conf)) ;; After pull, always check for orphaned files (if (= "pull" action) (act/cleanup-orphaned-files-with-pre-pull processed-conf (api/apis processed-conf)) diff --git a/test/mirthsync/apis_test.clj b/test/mirthsync/apis_test.clj index 9fd7a12..818710a 100644 --- a/test/mirthsync/apis_test.clj +++ b/test/mirthsync/apis_test.clj @@ -4,6 +4,7 @@ [clojure.test :as ct] [clojure.zip :as cz] [mirthsync.apis :as ma] + [mirthsync.interfaces] [mirthsync.cross-platform-utils :as cpu] [mirthsync.files :as mf] [mirthsync.fixture-tools :refer [build-path]] @@ -218,6 +219,54 @@ :alerts] (ma/apis {:disk-mode "groups" :include-configuration-map false}))))) +(ct/deftest bulk-deployment-tests + (ct/testing "Deploy all channels function creates correct XML" + (let [app-conf {:bulk-deploy-channels (atom ["channel1" "channel2" "channel3"])} + expected-xml "channel1channel2channel3"] + ;; Test that the XML structure is created correctly + ;; We can't easily test the actual HTTP call without mocking, but we can test the data structure + (ct/is (= ["channel1" "channel2" "channel3"] @(:bulk-deploy-channels app-conf))))) + + (ct/testing "Deploy all channels handles empty channel list" + (let [app-conf {:bulk-deploy-channels (atom [])}] + ;; Should handle empty list gracefully + (ct/is (empty? @(:bulk-deploy-channels app-conf))))) + + (ct/testing "Deploy all channels with nil atom" + (let [app-conf {:bulk-deploy-channels nil}] + ;; Should handle nil atom gracefully + (ct/is (nil? (:bulk-deploy-channels app-conf)))))) + +(ct/deftest channel-after-push-tests + (ct/testing "Channel after-push collects IDs for bulk deployment" + (let [app-conf {:deploy-all true :bulk-deploy-channels (atom [])} + api :channels + el-loc (mx/to-zip "test-channel-idTest Channel") + result {:status 200 :body "true"}] + ;; Mock the find-id function behavior + (with-redefs [mirthsync.interfaces/find-id (fn [_ _] "test-channel-id")] + (mirthsync.interfaces/after-push api (assoc app-conf :el-loc el-loc) result) + (ct/is (= ["test-channel-id"] @(:bulk-deploy-channels app-conf)))))) + + (ct/testing "Channel after-push doesn't collect IDs when deploy-all is false" + (let [app-conf {:deploy false :deploy-all false :bulk-deploy-channels (atom [])} + api :channels + el-loc (mx/to-zip "test-channel-idTest Channel") + result {:status 200 :body "true"}] + ;; Mock the find-id function behavior + (with-redefs [mirthsync.interfaces/find-id (fn [_ _] "test-channel-id")] + (mirthsync.interfaces/after-push api (assoc app-conf :el-loc el-loc) result) + (ct/is (empty? @(:bulk-deploy-channels app-conf)))))) + + (ct/testing "Channel after-push handles failed channel push" + (let [app-conf {:deploy-all true :bulk-deploy-channels (atom [])} + api :channels + el-loc (mx/to-zip "test-channel-idTest Channel") + result {:status 400 :body "error"}] + ;; When push fails, should not collect channel ID and should return false + (ct/is (= false (mirthsync.interfaces/after-push api (assoc app-conf :el-loc el-loc) result))) + (ct/is (empty? @(:bulk-deploy-channels app-conf)))))) + (comment (ct/deftest iterate-apis (ct/is (= "target/foo/blah.xm" (local-path-str "foo/blah.xml" "target"))))) diff --git a/test/mirthsync/cli_test.clj b/test/mirthsync/cli_test.clj index 65b9e56..fbf7503 100644 --- a/test/mirthsync/cli_test.clj +++ b/test/mirthsync/cli_test.clj @@ -77,6 +77,28 @@ (testing "Delete-orphaned defaults to false" (let [conf (config ["-s" "https://localhost:8443/api" "-u" "admin" "-p" "password" "-t" "foo" "pull"])] - (is (= false (:delete-orphaned conf)))))) + (is (= false (:delete-orphaned conf))))) + + (testing "Deploy flag is parsed correctly" + (let [conf (config ["-s" "https://localhost:8443/api" "-u" "admin" "-p" "password" "-t" "foo" "--deploy" "push"])] + (is (= true (:deploy conf))))) + + (testing "Deploy defaults to nil" + (let [conf (config ["-s" "https://localhost:8443/api" "-u" "admin" "-p" "password" "-t" "foo" "push"])] + (is (nil? (:deploy conf))))) + + (testing "Deploy-all flag is parsed correctly" + (let [conf (config ["-s" "https://localhost:8443/api" "-u" "admin" "-p" "password" "-t" "foo" "--deploy-all" "push"])] + (is (= true (:deploy-all conf))))) + + (testing "Deploy-all defaults to nil" + (let [conf (config ["-s" "https://localhost:8443/api" "-u" "admin" "-p" "password" "-t" "foo" "push"])] + (is (nil? (:deploy-all conf))))) + + (testing "Deploy and deploy-all are mutually exclusive options" + (let [conf-with-deploy (config ["-s" "https://localhost:8443/api" "-u" "admin" "-p" "password" "-t" "foo" "--deploy" "push"]) + conf-with-deploy-all (config ["-s" "https://localhost:8443/api" "-u" "admin" "-p" "password" "-t" "foo" "--deploy-all" "push"])] + (is (and (:deploy conf-with-deploy) (nil? (:deploy-all conf-with-deploy)))) + (is (and (:deploy-all conf-with-deploy-all) (nil? (:deploy conf-with-deploy-all))))))) diff --git a/test/mirthsync/common_tests.clj b/test/mirthsync/common_tests.clj index 2ce6049..b080859 100644 --- a/test/mirthsync/common_tests.clj +++ b/test/mirthsync/common_tests.clj @@ -1,7 +1,12 @@ (ns mirthsync.common-tests (:require [clojure.test :refer :all] + [clojure.string :as str] [mirthsync.core :refer :all] - [mirthsync.fixture-tools :refer :all])) + [mirthsync.fixture-tools :refer :all] + [mirthsync.http-client :as mhttp] + [mirthsync.interfaces :as mi] + [mirthsync.xml :as mx] + [mirthsync.cross-platform-utils :as cpu])) ;; NOTE - it's important that some of these tests run in order (defn test-integration @@ -287,3 +292,152 @@ ;; ignore a few extra line types ;; (diff "--recursive" "--suppress-common-lines" "-I" ".*.*" "-I" ".*