Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<value>` and `<comment>` 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`.
Expand Down
31 changes: 29 additions & 2 deletions src/mirthsync/apis.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.")))
Expand All @@ -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 "<set>"
(map #(str "<string>" % "</string>") channel-ids)
"</set>")]
(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))
Expand Down
6 changes: 5 additions & 1 deletion src/mirthsync/cli.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down
9 changes: 8 additions & 1 deletion src/mirthsync/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
49 changes: 49 additions & 0 deletions test/mirthsync/apis_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Expand Down Expand Up @@ -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 "<set><string>channel1</string><string>channel2</string><string>channel3</string></set>"]
;; 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 "<channel><id>test-channel-id</id><name>Test Channel</name></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 "<channel><id>test-channel-id</id><name>Test Channel</name></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 "<channel><id>test-channel-id</id><name>Test Channel</name></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")))))
24 changes: 23 additions & 1 deletion test/mirthsync/cli_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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)))))))


156 changes: 155 additions & 1 deletion test/mirthsync/common_tests.clj
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -287,3 +292,152 @@

;; ignore a few extra line types
;; (diff "--recursive" "--suppress-common-lines" "-I" ".*<contextType>.*" "-I" ".*<time>.*" "-I" ".*<timezone>.*" "-I" ".*<revision>.*" "-I" ".*version=\"[[:digit:]].[[:digit:]]\\+.[[:digit:]]\\+\".*" "-I" ".*<pruneErroredMessages>.*" repo-dir (str "dev-resources/mirth-" version "-baseline"))

(defn test-deployment-integration
[repo-dir baseline-dir version]

(testing "Deploy functionality - individual channel deployment"
;; Test that --deploy flag works for individual channel deployment
(let [deploy-test-dir (str repo-dir "-deploy-test")]
(ensure-directory-exists deploy-test-dir)

;; Push with individual deployment flag
(is (= 0 (main-func "-s" "https://localhost:8443/api"
"--restrict-to-path" (build-path "Channels" "This is a group" "Hello DB Writer.xml")
"-u" "admin" "-p" "admin" "-t" baseline-dir
"-i" "-f" "--deploy" "push")))

;; Verify the channel was deployed by pulling and checking
(is (= 0 (main-func "-s" "https://localhost:8443/api"
"-u" "admin" "-p" "admin" "-t" deploy-test-dir
"-i" "-f" "pull")))))

(testing "Deploy-all functionality - bulk channel deployment"
;; Test that --deploy-all flag works for bulk channel deployment
(let [deploy-all-test-dir (str repo-dir "-deploy-all-test")]
(ensure-directory-exists deploy-all-test-dir)

;; Push multiple channels with bulk deployment flag
(is (= 0 (main-func "-s" "https://localhost:8443/api"
"--restrict-to-path" (build-path "Channels" "This is a group")
"-u" "admin" "-p" "admin" "-t" baseline-dir
"-i" "-f" "--deploy-all" "push")))

;; Verify the channels were deployed by pulling and checking
(is (= 0 (main-func "-s" "https://localhost:8443/api"
"-u" "admin" "-p" "admin" "-t" deploy-all-test-dir
"-i" "-f" "pull")))))

(testing "Deploy options are mutually exclusive in practice"
;; Test that using both flags doesn't cause conflicts
;; (They can both be specified but only one should take effect based on implementation)
(let [mutual-test-dir (str repo-dir "-mutual-test")]
(ensure-directory-exists mutual-test-dir)

;; This should work without error - implementation should handle both flags gracefully
(is (= 0 (main-func "-s" "https://localhost:8443/api"
"--restrict-to-path" (build-path "Channels" "This is a group" "Hello DB Writer.xml")
"-u" "admin" "-p" "admin" "-t" baseline-dir
"-i" "-f" "--deploy" "--deploy-all" "push")))))

(testing "Comprehensive bulk deployment verification - undeploy via API, bulk deploy via mirthsync, verify deployment"
;; This test ensures the bulk deployment feature works end-to-end by:
;; 1. Undeploying all channels via direct API calls
;; 2. Using mirthsync to bulk deploy channels
;; 3. Verifying that channels are actually deployed by fetching them
(let [bulk-deploy-test-dir (str repo-dir "-bulk-deploy-verify")
verify-dir (str bulk-deploy-test-dir "-verify")]
(ensure-directory-exists bulk-deploy-test-dir)
(ensure-directory-exists verify-dir)

;; Step 1: First, pull channels to get their IDs for undeployment
(is (= 0 (main-func "-s" "https://localhost:8443/api"
"-u" "admin" "-p" "admin" "-t" bulk-deploy-test-dir
"-i" "-f" "--include-configuration-map" "pull")))

;; Step 2: Extract channel IDs from the pulled XML files
(let [channels-dir (build-path bulk-deploy-test-dir "Channels")
channel-ids (if (cpu/directory? channels-dir)
(let [channel-files (cpu/find-files channels-dir :type "f" :name "*.xml")
files (str/split channel-files #"\n")
xml-files (filter #(and (str/ends-with? % ".xml")
(not (str/ends-with? % "index.xml"))) files)]
(mapcat (fn [file]
(try
(let [content (slurp file)
xml-zip (mx/to-zip content)
id (mi/find-id :channels xml-zip)]
(when id [id]))
(catch Exception e
(println (str "Error parsing channel file " file ": " (.getMessage e)))
[])))
xml-files))
[])]

;; Step 3: Undeploy all channels via direct API call
(when (seq channel-ids)
(let [undeploy-xml (apply str "<set>"
(map #(str "<string>" % "</string>") channel-ids)
"</set>")]
(let [response (mhttp/with-authentication
{:server "https://localhost:8443/api"
:username "admin"
:password "admin"
:ignore-cert-warnings true}
(fn []
(mhttp/post-xml
{:server "https://localhost:8443/api"
:ignore-cert-warnings true}
"/channels/_undeploy"
undeploy-xml
{:returnErrors "true" :debug "false"}
false)))]
;; Verify undeploy was successful (204 No Content) or channels weren't deployed (404/400)
(is (contains? #{200 204 400 404} (:status response))
(str "Undeploy should succeed (200/204) or indicate channels weren't deployed (400/404), got: " (:status response))))))

;; Step 4: Push channels with bulk deployment flag
(is (= 0 (main-func "-s" "https://localhost:8443/api"
"--restrict-to-path" (build-path "Channels" "This is a group")
"-u" "admin" "-p" "admin" "-t" baseline-dir
"-i" "-f" "--deploy-all" "push")))

;; Step 5: Pull channels again to verify they are deployed
(is (= 0 (main-func "-s" "https://localhost:8443/api"
"-u" "admin" "-p" "admin" "-t" verify-dir
"-i" "-f" "--include-configuration-map" "pull")))

;; Step 6: Verify that channels were actually deployed by checking their status
;; We can verify this by checking if the channels exist and are accessible
(let [verify-channels-dir (build-path verify-dir "Channels")]
(when (cpu/directory? verify-channels-dir)
(let [channel-files (cpu/find-files verify-channels-dir :type "f" :name "*.xml")
files (str/split channel-files #"\n")
xml-files (filter #(and (str/ends-with? % ".xml")
(not (str/ends-with? % "index.xml"))) files)]
;; Verify that we have channel files
(is (not (empty? xml-files)) "Channels should be present after bulk deployment")

;; Verify that channels can be fetched (indicating they are deployed)
(doseq [file xml-files]
(let [content (slurp file)
xml-zip (mx/to-zip content)
channel-id (mi/find-id :channels xml-zip)
channel-name (mi/find-name :channels xml-zip)]
(when channel-id
;; Try to fetch the channel directly via API to verify it's deployed
(let [response (mhttp/with-authentication
{:server "https://localhost:8443/api"
:username "admin"
:password "admin"
:ignore-cert-warnings true}
(fn []
(mhttp/fetch-all
{:server "https://localhost:8443/api"
:ignore-cert-warnings true
:api :channels}
(fn [zip-loc]
(let [channels (mi/find-elements :channels zip-loc)]
(filter #(= channel-id (mi/find-id :channels %)) channels))))))]
(is (not (empty? response))
(str "Channel " channel-name " (ID: " channel-id ") should be accessible after bulk deployment")))))))))))))
Loading
Loading