diff --git a/.gitattributes b/.gitattributes index 1e727afd1f..f894780747 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,3 +11,7 @@ configure-from-env text eol=lf # Binary files are left untouched *.jar binary +*.dcm binary + +# NCPDP fixtures delimit with control characters (0x1C/0x1D/0x1E), not line breaks +/server/tests/test-ncpdp-*-input.txt binary diff --git a/ci/README.md b/ci/README.md index 660f4c6af0..709ce46cb0 100644 --- a/ci/README.md +++ b/ci/README.md @@ -42,18 +42,39 @@ the payload sent to the channel. The other files are optional assertions: | `source_sourcemap.yml` | Source map supplied with `source` | | `source_metadata.yml` | Selected source message metadata | | `source_status` | Source status | +| `source_raw` | Raw source payload as stored | | `source_response` | Source response payload | | `source_transformed` | Transformed source payload | +| `source_encoded` | Encoded source payload | +| `source_processing_error` | Processing error recorded against the source | | `destNN` | Sent payload for destination `NN` | | `destNN_transformed` | Transformed payload for destination `NN` | -| `destNN_response` | Response payload from destination `NN` | +| `destNN_response` | Response payload from destination `NN`, before its response transformer | +| `destNN_processed_response` | Response payload for destination `NN` after its response transformer | +| `destNN_processing_error` | Processing error recorded against destination `NN` | | `destNN_metadata.yml` | Selected metadata for destination `NN` | | `destNN_status` | Status for destination `NN` | +| `attachmentNN` | Content of attachment `NN` | +| `attachmentNN_type` | MIME type of attachment `NN` | `NN` is the destination connector's metadata ID: `dest01` is the first destination, `dest02` the second, and so on. Metadata files are YAML mappings; list -only the keys that matter to the test. Content assertions are byte-for-byte, except -that `((ANY))` matches variable content such as generated IDs or timestamps. +only the keys that matter to the test. A key whose value is `((NONE))` asserts that +nothing is stored under it, which is how a custom metadata column with no value is +asserted. A TIMESTAMP custom metadata column is compared as its UTC instant, so write +it as `2010-01-02T13:01:02Z`. Content assertions are byte-for-byte, except +that `((ANY))` matches variable content such as generated IDs or timestamps. A content +assertion file whose entire contents are `((NONE))` asserts the opposite: that the server +stored no such content at all, which is not the same as storing an empty string. + +For an attachment, `NN` is a position rather than an ID: `attachment01` is the attachment +whose `${ATTACH:id}` token appears first in the source raw content, `attachment02` the next, +and any attachment the message does not reference comes after those, ordered by ID. The +server's own list is ordered by ID, which is a generated UUID, so it is not something a +fixture can count along. An `attachmentNN` file holds raw bytes and is compared byte for +byte, so it can hold an image or any other binary attachment, and `((NONE))` in it asserts +that there is no attachment in that position - which is also how a fixture pins down how +many attachments there are. A fixture case runs in every configuration by default. To limit it, add a `configurations` file at the case root with one configuration name per line: @@ -87,6 +108,10 @@ Put hand-written JUnit 5 tests in the fixture tests and are appropriate for multi-step workflows, computed expectations, or assertions that do not fit the fixture files. +A Java test's channels go in `smoketest/src/test/resources/channels/`, not under +`ci/tests/`, which is the fixture generator's tree; `Harness.deploy` loads either from +the classpath. + ## Run Locally Run every configuration: diff --git a/ci/configurations/alpine-temurin21-mysql.compose.yml b/ci/configurations/alpine-temurin21-mysql.compose.yml index feec339209..a40998ed81 100644 --- a/ci/configurations/alpine-temurin21-mysql.compose.yml +++ b/ci/configurations/alpine-temurin21-mysql.compose.yml @@ -22,7 +22,7 @@ services: condition: service_healthy environment: DATABASE: mysql - DATABASE_URL: jdbc:mysql://db:3306/mirthdb + DATABASE_URL: jdbc:mysql://db:3306/mirthdb?sessionVariables=transaction_isolation='READ-COMMITTED' DATABASE_USERNAME: mirthdb DATABASE_PASSWORD: mirthdb DATABASE_MAX_CONNECTIONS: 20 diff --git a/ci/tests/101-raw-no-op/README.md b/ci/tests/101-raw-no-op/README.md new file mode 100644 index 0000000000..dc0eb215d6 --- /dev/null +++ b/ci/tests/101-raw-no-op/README.md @@ -0,0 +1,7 @@ +# 101-raw-no-op + +A channel that does nothing: the RAW data type at both ends, no filter or transformer elements, +Channel Reader to a Channel Writer templated on `${message.encodedData}`. + +With no filter or transformer the engine skips serialization altogether and the encoded content is +the raw content, so `dest01` asserts the payload reaches the destination unchanged. diff --git a/ci/tests/101-raw-no-op/channels/01-raw-no-op/messages/01-hello-world/source_encoded b/ci/tests/101-raw-no-op/channels/01-raw-no-op/messages/01-hello-world/source_encoded new file mode 100644 index 0000000000..6769dd60bd --- /dev/null +++ b/ci/tests/101-raw-no-op/channels/01-raw-no-op/messages/01-hello-world/source_encoded @@ -0,0 +1 @@ +Hello world! \ No newline at end of file diff --git a/ci/tests/110-hl7-no-op/README.md b/ci/tests/110-hl7-no-op/README.md new file mode 100644 index 0000000000..4d39054c5f --- /dev/null +++ b/ci/tests/110-hl7-no-op/README.md @@ -0,0 +1,8 @@ +# 110-hl7-no-op + +The same no-op channel as 101 with the HL7 v2 data type at both ends, responding with +`Auto-generate (Destinations completed)`. + +- `dest01_metadata.yml` - the custom metadata columns the HL7 serializer fills in from MSH-4 and + MSH-9 on its own, with no transformer having run. +- `source_response` - the generated ACK, echoing the inbound MSH fields and MSA-2. diff --git a/ci/tests/120-filter-transformer/README.md b/ci/tests/120-filter-transformer/README.md new file mode 100644 index 0000000000..831331927f --- /dev/null +++ b/ci/tests/120-filter-transformer/README.md @@ -0,0 +1,13 @@ +# 120-filter-transformer + +Filter and transformer outcomes, on the RAW data type so that the transformed and encoded content +are exactly what the script left in `msg`. + +- `01-source-filter` - a JavaScript rule rejecting messages containing `REJECT`. An accepted + message is TRANSFORMED and reaches `dest01`; a rejected one is FILTERED, and its transformed + content is stored all the same, because the filter runs after the content is handed to the script. +- `02-source-transformer` - a JavaScript step prefixing the message. The transformed content is the + script's output, and RAW encodes to it unchanged, so the destination receives the same thing. +- `03-transformer-error` - a step that throws, giving ERROR. + +The case of no filter and no transformer at all is 101 and 110. diff --git a/ci/tests/120-filter-transformer/channels/01-source-filter/channel.xml b/ci/tests/120-filter-transformer/channels/01-source-filter/channel.xml new file mode 100644 index 0000000000..a4f60aeca1 --- /dev/null +++ b/ci/tests/120-filter-transformer/channels/01-source-filter/channel.xml @@ -0,0 +1,192 @@ + + 8f1d2c40-1a11-4a1e-9f9b-2c0a5a4e1001 + 2 + filter-transformer-source-filter + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Reject flagged messages + 0 + true + NONE + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/ci/tests/120-filter-transformer/channels/01-source-filter/messages/01-accepted/dest01 b/ci/tests/120-filter-transformer/channels/01-source-filter/messages/01-accepted/dest01 new file mode 100644 index 0000000000..b0e8718fea --- /dev/null +++ b/ci/tests/120-filter-transformer/channels/01-source-filter/messages/01-accepted/dest01 @@ -0,0 +1 @@ +((ANY))KEEP this one((ANY)) \ No newline at end of file diff --git a/ci/tests/120-filter-transformer/channels/01-source-filter/messages/01-accepted/dest01_status b/ci/tests/120-filter-transformer/channels/01-source-filter/messages/01-accepted/dest01_status new file mode 100644 index 0000000000..b09a037dd2 --- /dev/null +++ b/ci/tests/120-filter-transformer/channels/01-source-filter/messages/01-accepted/dest01_status @@ -0,0 +1 @@ +SENT diff --git a/ci/tests/120-filter-transformer/channels/01-source-filter/messages/01-accepted/source b/ci/tests/120-filter-transformer/channels/01-source-filter/messages/01-accepted/source new file mode 100644 index 0000000000..c47e7ee86d --- /dev/null +++ b/ci/tests/120-filter-transformer/channels/01-source-filter/messages/01-accepted/source @@ -0,0 +1 @@ +KEEP this one \ No newline at end of file diff --git a/ci/tests/120-filter-transformer/channels/01-source-filter/messages/01-accepted/source_status b/ci/tests/120-filter-transformer/channels/01-source-filter/messages/01-accepted/source_status new file mode 100644 index 0000000000..ddc7ed8516 --- /dev/null +++ b/ci/tests/120-filter-transformer/channels/01-source-filter/messages/01-accepted/source_status @@ -0,0 +1 @@ +TRANSFORMED diff --git a/ci/tests/120-filter-transformer/channels/01-source-filter/messages/01-accepted/source_transformed b/ci/tests/120-filter-transformer/channels/01-source-filter/messages/01-accepted/source_transformed new file mode 100644 index 0000000000..c47e7ee86d --- /dev/null +++ b/ci/tests/120-filter-transformer/channels/01-source-filter/messages/01-accepted/source_transformed @@ -0,0 +1 @@ +KEEP this one \ No newline at end of file diff --git a/ci/tests/120-filter-transformer/channels/01-source-filter/messages/02-filtered/source b/ci/tests/120-filter-transformer/channels/01-source-filter/messages/02-filtered/source new file mode 100644 index 0000000000..d9e788e51f --- /dev/null +++ b/ci/tests/120-filter-transformer/channels/01-source-filter/messages/02-filtered/source @@ -0,0 +1 @@ +REJECT this one \ No newline at end of file diff --git a/ci/tests/120-filter-transformer/channels/01-source-filter/messages/02-filtered/source_status b/ci/tests/120-filter-transformer/channels/01-source-filter/messages/02-filtered/source_status new file mode 100644 index 0000000000..d8a5d8d473 --- /dev/null +++ b/ci/tests/120-filter-transformer/channels/01-source-filter/messages/02-filtered/source_status @@ -0,0 +1 @@ +FILTERED diff --git a/ci/tests/120-filter-transformer/channels/01-source-filter/messages/02-filtered/source_transformed b/ci/tests/120-filter-transformer/channels/01-source-filter/messages/02-filtered/source_transformed new file mode 100644 index 0000000000..d9e788e51f --- /dev/null +++ b/ci/tests/120-filter-transformer/channels/01-source-filter/messages/02-filtered/source_transformed @@ -0,0 +1 @@ +REJECT this one \ No newline at end of file diff --git a/ci/tests/120-filter-transformer/channels/02-source-transformer/channel.xml b/ci/tests/120-filter-transformer/channels/02-source-transformer/channel.xml new file mode 100644 index 0000000000..4ee544e3f6 --- /dev/null +++ b/ci/tests/120-filter-transformer/channels/02-source-transformer/channel.xml @@ -0,0 +1,191 @@ + + 8f1d2c40-1a11-4a1e-9f9b-2c0a5a4e1002 + 2 + filter-transformer-source-transformer + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + + Prefix the message + 0 + true + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/ci/tests/120-filter-transformer/channels/02-source-transformer/messages/01-transformed/dest01 b/ci/tests/120-filter-transformer/channels/02-source-transformer/messages/01-transformed/dest01 new file mode 100644 index 0000000000..8c38d64a5f --- /dev/null +++ b/ci/tests/120-filter-transformer/channels/02-source-transformer/messages/01-transformed/dest01 @@ -0,0 +1 @@ +((ANY))transformed:Hello world!((ANY)) \ No newline at end of file diff --git a/ci/tests/120-filter-transformer/channels/02-source-transformer/messages/01-transformed/dest01_status b/ci/tests/120-filter-transformer/channels/02-source-transformer/messages/01-transformed/dest01_status new file mode 100644 index 0000000000..b09a037dd2 --- /dev/null +++ b/ci/tests/120-filter-transformer/channels/02-source-transformer/messages/01-transformed/dest01_status @@ -0,0 +1 @@ +SENT diff --git a/ci/tests/120-filter-transformer/channels/02-source-transformer/messages/01-transformed/source b/ci/tests/120-filter-transformer/channels/02-source-transformer/messages/01-transformed/source new file mode 100644 index 0000000000..6769dd60bd --- /dev/null +++ b/ci/tests/120-filter-transformer/channels/02-source-transformer/messages/01-transformed/source @@ -0,0 +1 @@ +Hello world! \ No newline at end of file diff --git a/ci/tests/120-filter-transformer/channels/02-source-transformer/messages/01-transformed/source_status b/ci/tests/120-filter-transformer/channels/02-source-transformer/messages/01-transformed/source_status new file mode 100644 index 0000000000..ddc7ed8516 --- /dev/null +++ b/ci/tests/120-filter-transformer/channels/02-source-transformer/messages/01-transformed/source_status @@ -0,0 +1 @@ +TRANSFORMED diff --git a/ci/tests/120-filter-transformer/channels/02-source-transformer/messages/01-transformed/source_transformed b/ci/tests/120-filter-transformer/channels/02-source-transformer/messages/01-transformed/source_transformed new file mode 100644 index 0000000000..ae16d268c8 --- /dev/null +++ b/ci/tests/120-filter-transformer/channels/02-source-transformer/messages/01-transformed/source_transformed @@ -0,0 +1 @@ +transformed:Hello world! \ No newline at end of file diff --git a/ci/tests/120-filter-transformer/channels/03-transformer-error/channel.xml b/ci/tests/120-filter-transformer/channels/03-transformer-error/channel.xml new file mode 100644 index 0000000000..81ab98ae3e --- /dev/null +++ b/ci/tests/120-filter-transformer/channels/03-transformer-error/channel.xml @@ -0,0 +1,191 @@ + + 8f1d2c40-1a11-4a1e-9f9b-2c0a5a4e1003 + 2 + filter-transformer-transformer-error + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + + Always throw + 0 + true + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/ci/tests/120-filter-transformer/channels/03-transformer-error/messages/01-throws/source b/ci/tests/120-filter-transformer/channels/03-transformer-error/messages/01-throws/source new file mode 100644 index 0000000000..6769dd60bd --- /dev/null +++ b/ci/tests/120-filter-transformer/channels/03-transformer-error/messages/01-throws/source @@ -0,0 +1 @@ +Hello world! \ No newline at end of file diff --git a/ci/tests/120-filter-transformer/channels/03-transformer-error/messages/01-throws/source_status b/ci/tests/120-filter-transformer/channels/03-transformer-error/messages/01-throws/source_status new file mode 100644 index 0000000000..5df7507e2d --- /dev/null +++ b/ci/tests/120-filter-transformer/channels/03-transformer-error/messages/01-throws/source_status @@ -0,0 +1 @@ +ERROR diff --git a/ci/tests/130-hl7-strict-serialization/README.md b/ci/tests/130-hl7-strict-serialization/README.md new file mode 100644 index 0000000000..fcceb568ee --- /dev/null +++ b/ci/tests/130-hl7-strict-serialization/README.md @@ -0,0 +1,17 @@ +# 130-hl7-strict-serialization + +The HL7 v2 data type with `useStrictParser`, so the message goes through HAPI rather than being +read as delimited text. The channel's transformer step does nothing; it is there only because the +engine does not serialize at all without one. + +- `01-valid-adt` - an ADT^A01 is TRANSFORMED, and `source_transformed` asserts the whole strict + `urn:hl7-org:v2xml` document, which is what distinguishes strict parsing from the `` + form the non-strict reader produces. +- `02-not-hl7` - a payload HAPI cannot read gives ERROR rather than being passed through. +- `03-unknown-message-type` - HAPI reads the message but has no structure class for its + MSH-9, so it parses into a `GenericMessage` and serialization fails. Distinct from + `02-not-hl7`, where the parse itself fails. + +`02-strict-validating` is the same channel with `useStrictValidation` on as well, running the +same two messages. Validation changes neither outcome, which is worth pinning: an operator +reaching for stricter handling does not get different behaviour here. diff --git a/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/channel.xml b/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/channel.xml new file mode 100644 index 0000000000..e8114adcbb --- /dev/null +++ b/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/channel.xml @@ -0,0 +1,389 @@ + + 8f1d2c40-1a11-4a1e-9f9b-2c0a5a4e1301 + 2 + hl7-strict-parser + + 1 + + 0 + sourceConnector + + + + Auto-generate (Destinations completed) + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + + No-op + 0 + true + + + + HL7V2 + HL7V2 + + + true + true + true + false + false + \r + true + + + true + false + \r + + + MSH_Segment + + + + \r + AA + + AE + An Error Occurred Processing Message. + AR + Message Rejected. + false + yyyyMMddHHmmss.SSS + + + AA,CA + AE,CE + AR,CR + true + Destination_Encoded + + + + + + true + true + true + false + false + \r + true + + + true + false + \r + + + MSH_Segment + + + + \r + AA + + AE + An Error Occurred Processing Message. + AR + Message Rejected. + false + yyyyMMddHHmmss.SSS + + + AA,CA + AE,CE + AR,CR + true + Destination_Encoded + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + HL7V2 + HL7V2 + + + true + true + true + false + false + \r + true + + + true + false + \r + + + MSH_Segment + + + + \r + AA + + AE + An Error Occurred Processing Message. + AR + Message Rejected. + false + yyyyMMddHHmmss.SSS + + + AA,CA + AE,CE + AR,CR + true + Destination_Encoded + + + + + + true + true + true + false + false + \r + true + + + true + false + \r + + + MSH_Segment + + + + \r + AA + + AE + An Error Occurred Processing Message. + AR + Message Rejected. + false + yyyyMMddHHmmss.SSS + + + AA,CA + AE,CE + AR,CR + true + Destination_Encoded + + + + + + + HL7V2 + HL7V2 + + + true + true + true + false + false + \r + true + + + true + false + \r + + + MSH_Segment + + + + \r + AA + + AE + An Error Occurred Processing Message. + AR + Message Rejected. + false + yyyyMMddHHmmss.SSS + + + AA,CA + AE,CE + AR,CR + true + Destination_Encoded + + + + + + true + true + true + false + false + \r + true + + + true + false + \r + + + MSH_Segment + + + + \r + AA + + AE + An Error Occurred Processing Message. + AR + Message Rejected. + false + yyyyMMddHHmmss.SSS + + + AA,CA + AE,CE + AR,CR + true + Destination_Encoded + + + + + + + + Channel Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/messages/01-valid-adt/dest01_status b/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/messages/01-valid-adt/dest01_status new file mode 100644 index 0000000000..b09a037dd2 --- /dev/null +++ b/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/messages/01-valid-adt/dest01_status @@ -0,0 +1 @@ +SENT diff --git a/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/messages/01-valid-adt/source b/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/messages/01-valid-adt/source new file mode 100644 index 0000000000..3d27f88587 --- /dev/null +++ b/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/messages/01-valid-adt/source @@ -0,0 +1,3 @@ +MSH|^~\&|Some Other App|Some Other Facility|Ehrsource|Or|20250101120000||ADT^A01|Msgid99999|P|2.3 +EVN|A01|20250101120000 +PID|1||123456^^^Hospital^MR||SMITH^JOHN||19700101|M diff --git a/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/messages/01-valid-adt/source_status b/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/messages/01-valid-adt/source_status new file mode 100644 index 0000000000..ddc7ed8516 --- /dev/null +++ b/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/messages/01-valid-adt/source_status @@ -0,0 +1 @@ +TRANSFORMED diff --git a/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/messages/01-valid-adt/source_transformed b/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/messages/01-valid-adt/source_transformed new file mode 100644 index 0000000000..021997704b --- /dev/null +++ b/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/messages/01-valid-adt/source_transformed @@ -0,0 +1 @@ +|^~\&Some Other AppSome Other FacilityEhrsourceOr20250101120000ADTA01Msgid99999P2.3A01202501011200001123456HospitalMRSMITHJOHN19700101M \ No newline at end of file diff --git a/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/messages/02-not-hl7/source b/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/messages/02-not-hl7/source new file mode 100644 index 0000000000..9ff36e0dee --- /dev/null +++ b/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/messages/02-not-hl7/source @@ -0,0 +1 @@ +this is not an HL7 message \ No newline at end of file diff --git a/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/messages/02-not-hl7/source_status b/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/messages/02-not-hl7/source_status new file mode 100644 index 0000000000..5df7507e2d --- /dev/null +++ b/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/messages/02-not-hl7/source_status @@ -0,0 +1 @@ +ERROR diff --git a/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/messages/03-unknown-message-type/source b/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/messages/03-unknown-message-type/source new file mode 100644 index 0000000000..49b13ef108 --- /dev/null +++ b/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/messages/03-unknown-message-type/source @@ -0,0 +1 @@ +MSH|^~\&|SENDAPP|SENDFAC|RECVAPP|RECVFAC|20260101120000||XYZ^Q01^XYZ_Q01|MSG00003|P|2.4 \ No newline at end of file diff --git a/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/messages/03-unknown-message-type/source_status b/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/messages/03-unknown-message-type/source_status new file mode 100644 index 0000000000..17416ae33e --- /dev/null +++ b/ci/tests/130-hl7-strict-serialization/channels/01-strict-parser/messages/03-unknown-message-type/source_status @@ -0,0 +1 @@ +ERROR \ No newline at end of file diff --git a/ci/tests/130-hl7-strict-serialization/channels/02-strict-validating/channel.xml b/ci/tests/130-hl7-strict-serialization/channels/02-strict-validating/channel.xml new file mode 100644 index 0000000000..343b91daac --- /dev/null +++ b/ci/tests/130-hl7-strict-serialization/channels/02-strict-validating/channel.xml @@ -0,0 +1,389 @@ + + c93a7f18-64b2-4e05-9d7a-1f8c3e5b0a42 + 2 + hl7-strict-parser-validating + + 1 + + 0 + sourceConnector + + + + Auto-generate (Destinations completed) + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + + No-op + 0 + true + + + + HL7V2 + HL7V2 + + + true + true + true + true + false + \r + true + + + true + true + \r + + + MSH_Segment + + + + \r + AA + + AE + An Error Occurred Processing Message. + AR + Message Rejected. + false + yyyyMMddHHmmss.SSS + + + AA,CA + AE,CE + AR,CR + true + Destination_Encoded + + + + + + true + true + true + true + false + \r + true + + + true + true + \r + + + MSH_Segment + + + + \r + AA + + AE + An Error Occurred Processing Message. + AR + Message Rejected. + false + yyyyMMddHHmmss.SSS + + + AA,CA + AE,CE + AR,CR + true + Destination_Encoded + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + HL7V2 + HL7V2 + + + true + true + true + true + false + \r + true + + + true + true + \r + + + MSH_Segment + + + + \r + AA + + AE + An Error Occurred Processing Message. + AR + Message Rejected. + false + yyyyMMddHHmmss.SSS + + + AA,CA + AE,CE + AR,CR + true + Destination_Encoded + + + + + + true + true + true + true + false + \r + true + + + true + true + \r + + + MSH_Segment + + + + \r + AA + + AE + An Error Occurred Processing Message. + AR + Message Rejected. + false + yyyyMMddHHmmss.SSS + + + AA,CA + AE,CE + AR,CR + true + Destination_Encoded + + + + + + + HL7V2 + HL7V2 + + + true + true + true + true + false + \r + true + + + true + true + \r + + + MSH_Segment + + + + \r + AA + + AE + An Error Occurred Processing Message. + AR + Message Rejected. + false + yyyyMMddHHmmss.SSS + + + AA,CA + AE,CE + AR,CR + true + Destination_Encoded + + + + + + true + true + true + true + false + \r + true + + + true + true + \r + + + MSH_Segment + + + + \r + AA + + AE + An Error Occurred Processing Message. + AR + Message Rejected. + false + yyyyMMddHHmmss.SSS + + + AA,CA + AE,CE + AR,CR + true + Destination_Encoded + + + + + + + + Channel Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/ci/tests/130-hl7-strict-serialization/channels/02-strict-validating/messages/01-valid-adt/source b/ci/tests/130-hl7-strict-serialization/channels/02-strict-validating/messages/01-valid-adt/source new file mode 100644 index 0000000000..3d27f88587 --- /dev/null +++ b/ci/tests/130-hl7-strict-serialization/channels/02-strict-validating/messages/01-valid-adt/source @@ -0,0 +1,3 @@ +MSH|^~\&|Some Other App|Some Other Facility|Ehrsource|Or|20250101120000||ADT^A01|Msgid99999|P|2.3 +EVN|A01|20250101120000 +PID|1||123456^^^Hospital^MR||SMITH^JOHN||19700101|M diff --git a/ci/tests/130-hl7-strict-serialization/channels/02-strict-validating/messages/01-valid-adt/source_status b/ci/tests/130-hl7-strict-serialization/channels/02-strict-validating/messages/01-valid-adt/source_status new file mode 100644 index 0000000000..ddc7ed8516 --- /dev/null +++ b/ci/tests/130-hl7-strict-serialization/channels/02-strict-validating/messages/01-valid-adt/source_status @@ -0,0 +1 @@ +TRANSFORMED diff --git a/ci/tests/130-hl7-strict-serialization/channels/02-strict-validating/messages/01-valid-adt/source_transformed b/ci/tests/130-hl7-strict-serialization/channels/02-strict-validating/messages/01-valid-adt/source_transformed new file mode 100644 index 0000000000..021997704b --- /dev/null +++ b/ci/tests/130-hl7-strict-serialization/channels/02-strict-validating/messages/01-valid-adt/source_transformed @@ -0,0 +1 @@ +|^~\&Some Other AppSome Other FacilityEhrsourceOr20250101120000ADTA01Msgid99999P2.3A01202501011200001123456HospitalMRSMITHJOHN19700101M \ No newline at end of file diff --git a/ci/tests/130-hl7-strict-serialization/channels/02-strict-validating/messages/03-unknown-message-type/source b/ci/tests/130-hl7-strict-serialization/channels/02-strict-validating/messages/03-unknown-message-type/source new file mode 100644 index 0000000000..49b13ef108 --- /dev/null +++ b/ci/tests/130-hl7-strict-serialization/channels/02-strict-validating/messages/03-unknown-message-type/source @@ -0,0 +1 @@ +MSH|^~\&|SENDAPP|SENDFAC|RECVAPP|RECVFAC|20260101120000||XYZ^Q01^XYZ_Q01|MSG00003|P|2.4 \ No newline at end of file diff --git a/ci/tests/130-hl7-strict-serialization/channels/02-strict-validating/messages/03-unknown-message-type/source_status b/ci/tests/130-hl7-strict-serialization/channels/02-strict-validating/messages/03-unknown-message-type/source_status new file mode 100644 index 0000000000..17416ae33e --- /dev/null +++ b/ci/tests/130-hl7-strict-serialization/channels/02-strict-validating/messages/03-unknown-message-type/source_status @@ -0,0 +1 @@ +ERROR \ No newline at end of file diff --git a/ci/tests/140-pre-post-processor/README.md b/ci/tests/140-pre-post-processor/README.md new file mode 100644 index 0000000000..733ed3ec6b --- /dev/null +++ b/ci/tests/140-pre-post-processor/README.md @@ -0,0 +1,8 @@ +# 140-pre-post-processor + +A no-op RAW channel carrying a pre-processor and a post-processor, responding with `Postprocessor`. + +- `dest01` - the pre-processor's return value. With no filter or transformer the encoded content is + the processed raw content rather than the raw content, so this is also what proves the + pre-processor ran. +- `source_response` - the post-processor's return value. diff --git a/ci/tests/140-pre-post-processor/channels/01-scripts/channel.xml b/ci/tests/140-pre-post-processor/channels/01-scripts/channel.xml new file mode 100644 index 0000000000..3f316d134a --- /dev/null +++ b/ci/tests/140-pre-post-processor/channels/01-scripts/channel.xml @@ -0,0 +1,181 @@ + + 8f1d2c40-1a11-4a1e-9f9b-2c0a5a4e1401 + 2 + pre-post-processor + + 1 + + 0 + sourceConnector + + + + Postprocessor + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Writer + DESTINATION + true + true + + + return 'pre:' + message; + return 'postprocessed'; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/ci/tests/140-pre-post-processor/channels/01-scripts/messages/01-scripts-run/dest01 b/ci/tests/140-pre-post-processor/channels/01-scripts/messages/01-scripts-run/dest01 new file mode 100644 index 0000000000..b72f7c045a --- /dev/null +++ b/ci/tests/140-pre-post-processor/channels/01-scripts/messages/01-scripts-run/dest01 @@ -0,0 +1 @@ +((ANY))pre:Hello world!((ANY)) \ No newline at end of file diff --git a/ci/tests/140-pre-post-processor/channels/01-scripts/messages/01-scripts-run/dest01_status b/ci/tests/140-pre-post-processor/channels/01-scripts/messages/01-scripts-run/dest01_status new file mode 100644 index 0000000000..b09a037dd2 --- /dev/null +++ b/ci/tests/140-pre-post-processor/channels/01-scripts/messages/01-scripts-run/dest01_status @@ -0,0 +1 @@ +SENT diff --git a/ci/tests/140-pre-post-processor/channels/01-scripts/messages/01-scripts-run/source b/ci/tests/140-pre-post-processor/channels/01-scripts/messages/01-scripts-run/source new file mode 100644 index 0000000000..6769dd60bd --- /dev/null +++ b/ci/tests/140-pre-post-processor/channels/01-scripts/messages/01-scripts-run/source @@ -0,0 +1 @@ +Hello world! \ No newline at end of file diff --git a/ci/tests/140-pre-post-processor/channels/01-scripts/messages/01-scripts-run/source_response b/ci/tests/140-pre-post-processor/channels/01-scripts/messages/01-scripts-run/source_response new file mode 100644 index 0000000000..366af71490 --- /dev/null +++ b/ci/tests/140-pre-post-processor/channels/01-scripts/messages/01-scripts-run/source_response @@ -0,0 +1 @@ +postprocessed \ No newline at end of file diff --git a/ci/tests/140-pre-post-processor/channels/01-scripts/messages/01-scripts-run/source_status b/ci/tests/140-pre-post-processor/channels/01-scripts/messages/01-scripts-run/source_status new file mode 100644 index 0000000000..ddc7ed8516 --- /dev/null +++ b/ci/tests/140-pre-post-processor/channels/01-scripts/messages/01-scripts-run/source_status @@ -0,0 +1 @@ +TRANSFORMED diff --git a/ci/tests/150-outbound-serialization-failure/README.md b/ci/tests/150-outbound-serialization-failure/README.md new file mode 100644 index 0000000000..8a5064ff2b --- /dev/null +++ b/ci/tests/150-outbound-serialization-failure/README.md @@ -0,0 +1,12 @@ +# 150-outbound-serialization-failure + +What the engine leaves behind when the transformed content is something the *outbound* data type +cannot serialize. The channel reads RAW and writes HL7 v2, so `msg` is the submitted payload +verbatim and the payload alone decides whether the outbound serializer succeeds — the one +arrangement in which a message the harness can submit reaches that failure. + +- `01-serializable` - an `` XML document, as the control: `source_encoded` is the ER7 + the outbound serializer produced from it, so the channel is known to work for content it accepts. +- `02-not-xml` - a payload the outbound serializer cannot parse. The source is ERROR, the + transformed content is stored all the same, and no encoded content exists at all — `((NONE))`, + which is not the same as an empty string. diff --git a/ci/tests/150-outbound-serialization-failure/channels/01-raw-to-hl7/channel.xml b/ci/tests/150-outbound-serialization-failure/channels/01-raw-to-hl7/channel.xml new file mode 100644 index 0000000000..547eac8210 --- /dev/null +++ b/ci/tests/150-outbound-serialization-failure/channels/01-raw-to-hl7/channel.xml @@ -0,0 +1,224 @@ + + 8f1d2c40-1a11-4a1e-9f9b-2c0a5a4e1501 + 2 + outbound-serialization-raw-to-hl7 + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + + No-op + 0 + true + + + + RAW + HL7V2 + + + JavaScript + + + + + + true + true + false + false + false + \n + true + + + false + false + \n + + + MSH_Segment + + + + \n + AA + + AE + An Error Occurred Processing Message. + AR + Message Rejected. + false + yyyyMMddHHmmss.SSS + + + AA,CA + AE,CE + AR,CR + true + Destination_Encoded + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + diff --git a/ci/tests/150-outbound-serialization-failure/channels/01-raw-to-hl7/messages/01-serializable/source b/ci/tests/150-outbound-serialization-failure/channels/01-raw-to-hl7/messages/01-serializable/source new file mode 100644 index 0000000000..89d90984c6 --- /dev/null +++ b/ci/tests/150-outbound-serialization-failure/channels/01-raw-to-hl7/messages/01-serializable/source @@ -0,0 +1 @@ +|^~\&DestinationADTA01Msgid99999P2.3222333SmithAlice \ No newline at end of file diff --git a/ci/tests/150-outbound-serialization-failure/channels/01-raw-to-hl7/messages/01-serializable/source_encoded b/ci/tests/150-outbound-serialization-failure/channels/01-raw-to-hl7/messages/01-serializable/source_encoded new file mode 100644 index 0000000000..f478cb8a1f --- /dev/null +++ b/ci/tests/150-outbound-serialization-failure/channels/01-raw-to-hl7/messages/01-serializable/source_encoded @@ -0,0 +1,2 @@ +MSH|^~\&|Destination||||||ADT^A01|Msgid99999|P|2.3 +PID|||222333||Smith^Alice diff --git a/ci/tests/150-outbound-serialization-failure/channels/01-raw-to-hl7/messages/02-not-xml/source b/ci/tests/150-outbound-serialization-failure/channels/01-raw-to-hl7/messages/02-not-xml/source new file mode 100644 index 0000000000..dc9024240e --- /dev/null +++ b/ci/tests/150-outbound-serialization-failure/channels/01-raw-to-hl7/messages/02-not-xml/source @@ -0,0 +1 @@ +this is not xml \ No newline at end of file diff --git a/ci/tests/150-outbound-serialization-failure/channels/01-raw-to-hl7/messages/02-not-xml/source_encoded b/ci/tests/150-outbound-serialization-failure/channels/01-raw-to-hl7/messages/02-not-xml/source_encoded new file mode 100644 index 0000000000..20769dab04 --- /dev/null +++ b/ci/tests/150-outbound-serialization-failure/channels/01-raw-to-hl7/messages/02-not-xml/source_encoded @@ -0,0 +1 @@ +((NONE)) \ No newline at end of file diff --git a/ci/tests/150-outbound-serialization-failure/channels/01-raw-to-hl7/messages/02-not-xml/source_processing_error b/ci/tests/150-outbound-serialization-failure/channels/01-raw-to-hl7/messages/02-not-xml/source_processing_error new file mode 100644 index 0000000000..a2511a15d4 --- /dev/null +++ b/ci/tests/150-outbound-serialization-failure/channels/01-raw-to-hl7/messages/02-not-xml/source_processing_error @@ -0,0 +1,3 @@ +ER7Serializer error +ERROR MESSAGE: Error converting XML to ER7 +((ANY)) \ No newline at end of file diff --git a/ci/tests/150-outbound-serialization-failure/channels/01-raw-to-hl7/messages/02-not-xml/source_status b/ci/tests/150-outbound-serialization-failure/channels/01-raw-to-hl7/messages/02-not-xml/source_status new file mode 100644 index 0000000000..17416ae33e --- /dev/null +++ b/ci/tests/150-outbound-serialization-failure/channels/01-raw-to-hl7/messages/02-not-xml/source_status @@ -0,0 +1 @@ +ERROR \ No newline at end of file diff --git a/ci/tests/150-outbound-serialization-failure/channels/01-raw-to-hl7/messages/02-not-xml/source_transformed b/ci/tests/150-outbound-serialization-failure/channels/01-raw-to-hl7/messages/02-not-xml/source_transformed new file mode 100644 index 0000000000..dc9024240e --- /dev/null +++ b/ci/tests/150-outbound-serialization-failure/channels/01-raw-to-hl7/messages/02-not-xml/source_transformed @@ -0,0 +1 @@ +this is not xml \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/README.md b/ci/tests/160-message-storage-levels/README.md new file mode 100644 index 0000000000..eb842cdb1f --- /dev/null +++ b/ci/tests/160-message-storage-levels/README.md @@ -0,0 +1,16 @@ +# 160-message-storage-levels + +Which content rows a channel's message storage setting keeps. Every channel here is the same +RAW-to-RAW channel with one JavaScript transformer step — so raw, transformed, encoded and sent +all exist to be stored — and differs only in ``. + +- `01-development` - DEVELOPMENT keeps everything: raw, transformed, encoded and the sent content. +- `02-production` - PRODUCTION drops transformed (and processed raw), keeping raw, encoded and + sent. This is the one level where content is partly kept and partly dropped. +- `03-raw` - RAW keeps only the raw content. Transformed, encoded and sent are gone, but the + statuses show the message still ran all the way to SENT. +- `04-metadata` - METADATA keeps no content at all, only the message and connector message rows, + so the statuses are all that is left to assert. + +DISABLED, the fifth level, stores nothing whatsoever — not even the message row a fixture's +assertions need a message to run against — so it lives in `MessageStorageDisabledTest` instead. diff --git a/ci/tests/160-message-storage-levels/channels/01-development/channel.xml b/ci/tests/160-message-storage-levels/channels/01-development/channel.xml new file mode 100644 index 0000000000..76adeff548 --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/01-development/channel.xml @@ -0,0 +1,191 @@ + + 8f1d2c40-1a11-4a1e-9f9b-2c0a5a4e1601 + 2 + message-storage-development + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + + Prefix the message + 0 + true + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/01-development/messages/01-every-stage-stored/dest01 b/ci/tests/160-message-storage-levels/channels/01-development/messages/01-every-stage-stored/dest01 new file mode 100644 index 0000000000..8c38d64a5f --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/01-development/messages/01-every-stage-stored/dest01 @@ -0,0 +1 @@ +((ANY))transformed:Hello world!((ANY)) \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/01-development/messages/01-every-stage-stored/source b/ci/tests/160-message-storage-levels/channels/01-development/messages/01-every-stage-stored/source new file mode 100644 index 0000000000..6769dd60bd --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/01-development/messages/01-every-stage-stored/source @@ -0,0 +1 @@ +Hello world! \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/01-development/messages/01-every-stage-stored/source_encoded b/ci/tests/160-message-storage-levels/channels/01-development/messages/01-every-stage-stored/source_encoded new file mode 100644 index 0000000000..ae16d268c8 --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/01-development/messages/01-every-stage-stored/source_encoded @@ -0,0 +1 @@ +transformed:Hello world! \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/01-development/messages/01-every-stage-stored/source_raw b/ci/tests/160-message-storage-levels/channels/01-development/messages/01-every-stage-stored/source_raw new file mode 100644 index 0000000000..6769dd60bd --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/01-development/messages/01-every-stage-stored/source_raw @@ -0,0 +1 @@ +Hello world! \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/01-development/messages/01-every-stage-stored/source_transformed b/ci/tests/160-message-storage-levels/channels/01-development/messages/01-every-stage-stored/source_transformed new file mode 100644 index 0000000000..ae16d268c8 --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/01-development/messages/01-every-stage-stored/source_transformed @@ -0,0 +1 @@ +transformed:Hello world! \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/02-production/channel.xml b/ci/tests/160-message-storage-levels/channels/02-production/channel.xml new file mode 100644 index 0000000000..baca4b55e7 --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/02-production/channel.xml @@ -0,0 +1,191 @@ + + 8f1d2c40-1a11-4a1e-9f9b-2c0a5a4e1602 + 2 + message-storage-production + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + + Prefix the message + 0 + true + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + PRODUCTION + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/02-production/messages/01-transformed-dropped/dest01 b/ci/tests/160-message-storage-levels/channels/02-production/messages/01-transformed-dropped/dest01 new file mode 100644 index 0000000000..8c38d64a5f --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/02-production/messages/01-transformed-dropped/dest01 @@ -0,0 +1 @@ +((ANY))transformed:Hello world!((ANY)) \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/02-production/messages/01-transformed-dropped/source b/ci/tests/160-message-storage-levels/channels/02-production/messages/01-transformed-dropped/source new file mode 100644 index 0000000000..6769dd60bd --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/02-production/messages/01-transformed-dropped/source @@ -0,0 +1 @@ +Hello world! \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/02-production/messages/01-transformed-dropped/source_encoded b/ci/tests/160-message-storage-levels/channels/02-production/messages/01-transformed-dropped/source_encoded new file mode 100644 index 0000000000..ae16d268c8 --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/02-production/messages/01-transformed-dropped/source_encoded @@ -0,0 +1 @@ +transformed:Hello world! \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/02-production/messages/01-transformed-dropped/source_raw b/ci/tests/160-message-storage-levels/channels/02-production/messages/01-transformed-dropped/source_raw new file mode 100644 index 0000000000..6769dd60bd --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/02-production/messages/01-transformed-dropped/source_raw @@ -0,0 +1 @@ +Hello world! \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/02-production/messages/01-transformed-dropped/source_transformed b/ci/tests/160-message-storage-levels/channels/02-production/messages/01-transformed-dropped/source_transformed new file mode 100644 index 0000000000..20769dab04 --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/02-production/messages/01-transformed-dropped/source_transformed @@ -0,0 +1 @@ +((NONE)) \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/03-raw/channel.xml b/ci/tests/160-message-storage-levels/channels/03-raw/channel.xml new file mode 100644 index 0000000000..d1fcf30d69 --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/03-raw/channel.xml @@ -0,0 +1,191 @@ + + 8f1d2c40-1a11-4a1e-9f9b-2c0a5a4e1603 + 2 + message-storage-raw + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + + Prefix the message + 0 + true + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + RAW + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/03-raw/messages/01-raw-only/dest01 b/ci/tests/160-message-storage-levels/channels/03-raw/messages/01-raw-only/dest01 new file mode 100644 index 0000000000..20769dab04 --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/03-raw/messages/01-raw-only/dest01 @@ -0,0 +1 @@ +((NONE)) \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/03-raw/messages/01-raw-only/dest01_status b/ci/tests/160-message-storage-levels/channels/03-raw/messages/01-raw-only/dest01_status new file mode 100644 index 0000000000..59586dad7f --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/03-raw/messages/01-raw-only/dest01_status @@ -0,0 +1 @@ +SENT \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/03-raw/messages/01-raw-only/source b/ci/tests/160-message-storage-levels/channels/03-raw/messages/01-raw-only/source new file mode 100644 index 0000000000..6769dd60bd --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/03-raw/messages/01-raw-only/source @@ -0,0 +1 @@ +Hello world! \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/03-raw/messages/01-raw-only/source_encoded b/ci/tests/160-message-storage-levels/channels/03-raw/messages/01-raw-only/source_encoded new file mode 100644 index 0000000000..20769dab04 --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/03-raw/messages/01-raw-only/source_encoded @@ -0,0 +1 @@ +((NONE)) \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/03-raw/messages/01-raw-only/source_raw b/ci/tests/160-message-storage-levels/channels/03-raw/messages/01-raw-only/source_raw new file mode 100644 index 0000000000..6769dd60bd --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/03-raw/messages/01-raw-only/source_raw @@ -0,0 +1 @@ +Hello world! \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/03-raw/messages/01-raw-only/source_status b/ci/tests/160-message-storage-levels/channels/03-raw/messages/01-raw-only/source_status new file mode 100644 index 0000000000..1316c06a69 --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/03-raw/messages/01-raw-only/source_status @@ -0,0 +1 @@ +TRANSFORMED \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/03-raw/messages/01-raw-only/source_transformed b/ci/tests/160-message-storage-levels/channels/03-raw/messages/01-raw-only/source_transformed new file mode 100644 index 0000000000..20769dab04 --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/03-raw/messages/01-raw-only/source_transformed @@ -0,0 +1 @@ +((NONE)) \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/04-metadata/channel.xml b/ci/tests/160-message-storage-levels/channels/04-metadata/channel.xml new file mode 100644 index 0000000000..4c409008c2 --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/04-metadata/channel.xml @@ -0,0 +1,191 @@ + + 8f1d2c40-1a11-4a1e-9f9b-2c0a5a4e1604 + 2 + message-storage-metadata + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + + Prefix the message + 0 + true + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + METADATA + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/04-metadata/messages/01-no-content/dest01 b/ci/tests/160-message-storage-levels/channels/04-metadata/messages/01-no-content/dest01 new file mode 100644 index 0000000000..20769dab04 --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/04-metadata/messages/01-no-content/dest01 @@ -0,0 +1 @@ +((NONE)) \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/04-metadata/messages/01-no-content/dest01_status b/ci/tests/160-message-storage-levels/channels/04-metadata/messages/01-no-content/dest01_status new file mode 100644 index 0000000000..59586dad7f --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/04-metadata/messages/01-no-content/dest01_status @@ -0,0 +1 @@ +SENT \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/04-metadata/messages/01-no-content/source b/ci/tests/160-message-storage-levels/channels/04-metadata/messages/01-no-content/source new file mode 100644 index 0000000000..6769dd60bd --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/04-metadata/messages/01-no-content/source @@ -0,0 +1 @@ +Hello world! \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/04-metadata/messages/01-no-content/source_encoded b/ci/tests/160-message-storage-levels/channels/04-metadata/messages/01-no-content/source_encoded new file mode 100644 index 0000000000..20769dab04 --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/04-metadata/messages/01-no-content/source_encoded @@ -0,0 +1 @@ +((NONE)) \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/04-metadata/messages/01-no-content/source_raw b/ci/tests/160-message-storage-levels/channels/04-metadata/messages/01-no-content/source_raw new file mode 100644 index 0000000000..20769dab04 --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/04-metadata/messages/01-no-content/source_raw @@ -0,0 +1 @@ +((NONE)) \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/04-metadata/messages/01-no-content/source_status b/ci/tests/160-message-storage-levels/channels/04-metadata/messages/01-no-content/source_status new file mode 100644 index 0000000000..1316c06a69 --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/04-metadata/messages/01-no-content/source_status @@ -0,0 +1 @@ +TRANSFORMED \ No newline at end of file diff --git a/ci/tests/160-message-storage-levels/channels/04-metadata/messages/01-no-content/source_transformed b/ci/tests/160-message-storage-levels/channels/04-metadata/messages/01-no-content/source_transformed new file mode 100644 index 0000000000..20769dab04 --- /dev/null +++ b/ci/tests/160-message-storage-levels/channels/04-metadata/messages/01-no-content/source_transformed @@ -0,0 +1 @@ +((NONE)) \ No newline at end of file diff --git a/ci/tests/170-content-removal/README.md b/ci/tests/170-content-removal/README.md new file mode 100644 index 0000000000..2ae5d3b5b2 --- /dev/null +++ b/ci/tests/170-content-removal/README.md @@ -0,0 +1,15 @@ +# 170-content-removal + +`removeContentOnCompletion`: a channel that stores its content normally while the message is in +flight and deletes it once every connector has completed. Both channels below are the +DEVELOPMENT-storage channel of 160 with that flag turned on, so 160's `01-development` is the +control showing what is there to be removed. + +- `01-unqueued` - the destination is not queued, so the engine removes content in the same + transaction that marks the message processed. +- `02-queued-destination` - the destination is queued, which forces the removal into a separate + transaction committed after the destination is already SENT. The content ends up equally gone; + this is the path that reaches the delete from the queue thread rather than the process thread. + +In both cases the statuses survive the removal, which is what separates removed content from a +channel that never stored any. diff --git a/ci/tests/170-content-removal/channels/01-unqueued/channel.xml b/ci/tests/170-content-removal/channels/01-unqueued/channel.xml new file mode 100644 index 0000000000..c6b170a8c8 --- /dev/null +++ b/ci/tests/170-content-removal/channels/01-unqueued/channel.xml @@ -0,0 +1,191 @@ + + 8f1d2c40-1a11-4a1e-9f9b-2c0a5a4e1701 + 2 + content-removal-unqueued + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + + Prefix the message + 0 + true + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + true + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/ci/tests/170-content-removal/channels/01-unqueued/messages/01-content-removed/dest01 b/ci/tests/170-content-removal/channels/01-unqueued/messages/01-content-removed/dest01 new file mode 100644 index 0000000000..20769dab04 --- /dev/null +++ b/ci/tests/170-content-removal/channels/01-unqueued/messages/01-content-removed/dest01 @@ -0,0 +1 @@ +((NONE)) \ No newline at end of file diff --git a/ci/tests/170-content-removal/channels/01-unqueued/messages/01-content-removed/dest01_status b/ci/tests/170-content-removal/channels/01-unqueued/messages/01-content-removed/dest01_status new file mode 100644 index 0000000000..59586dad7f --- /dev/null +++ b/ci/tests/170-content-removal/channels/01-unqueued/messages/01-content-removed/dest01_status @@ -0,0 +1 @@ +SENT \ No newline at end of file diff --git a/ci/tests/170-content-removal/channels/01-unqueued/messages/01-content-removed/source b/ci/tests/170-content-removal/channels/01-unqueued/messages/01-content-removed/source new file mode 100644 index 0000000000..6769dd60bd --- /dev/null +++ b/ci/tests/170-content-removal/channels/01-unqueued/messages/01-content-removed/source @@ -0,0 +1 @@ +Hello world! \ No newline at end of file diff --git a/ci/tests/170-content-removal/channels/01-unqueued/messages/01-content-removed/source_encoded b/ci/tests/170-content-removal/channels/01-unqueued/messages/01-content-removed/source_encoded new file mode 100644 index 0000000000..20769dab04 --- /dev/null +++ b/ci/tests/170-content-removal/channels/01-unqueued/messages/01-content-removed/source_encoded @@ -0,0 +1 @@ +((NONE)) \ No newline at end of file diff --git a/ci/tests/170-content-removal/channels/01-unqueued/messages/01-content-removed/source_raw b/ci/tests/170-content-removal/channels/01-unqueued/messages/01-content-removed/source_raw new file mode 100644 index 0000000000..20769dab04 --- /dev/null +++ b/ci/tests/170-content-removal/channels/01-unqueued/messages/01-content-removed/source_raw @@ -0,0 +1 @@ +((NONE)) \ No newline at end of file diff --git a/ci/tests/170-content-removal/channels/01-unqueued/messages/01-content-removed/source_status b/ci/tests/170-content-removal/channels/01-unqueued/messages/01-content-removed/source_status new file mode 100644 index 0000000000..1316c06a69 --- /dev/null +++ b/ci/tests/170-content-removal/channels/01-unqueued/messages/01-content-removed/source_status @@ -0,0 +1 @@ +TRANSFORMED \ No newline at end of file diff --git a/ci/tests/170-content-removal/channels/01-unqueued/messages/01-content-removed/source_transformed b/ci/tests/170-content-removal/channels/01-unqueued/messages/01-content-removed/source_transformed new file mode 100644 index 0000000000..20769dab04 --- /dev/null +++ b/ci/tests/170-content-removal/channels/01-unqueued/messages/01-content-removed/source_transformed @@ -0,0 +1 @@ +((NONE)) \ No newline at end of file diff --git a/ci/tests/170-content-removal/channels/02-queued-destination/channel.xml b/ci/tests/170-content-removal/channels/02-queued-destination/channel.xml new file mode 100644 index 0000000000..fe476aef4f --- /dev/null +++ b/ci/tests/170-content-removal/channels/02-queued-destination/channel.xml @@ -0,0 +1,191 @@ + + 8f1d2c40-1a11-4a1e-9f9b-2c0a5a4e1702 + 2 + content-removal-queued-destination + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + + Prefix the message + 0 + true + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + true + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + true + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/ci/tests/170-content-removal/channels/02-queued-destination/messages/01-content-removed/dest01 b/ci/tests/170-content-removal/channels/02-queued-destination/messages/01-content-removed/dest01 new file mode 100644 index 0000000000..20769dab04 --- /dev/null +++ b/ci/tests/170-content-removal/channels/02-queued-destination/messages/01-content-removed/dest01 @@ -0,0 +1 @@ +((NONE)) \ No newline at end of file diff --git a/ci/tests/170-content-removal/channels/02-queued-destination/messages/01-content-removed/dest01_status b/ci/tests/170-content-removal/channels/02-queued-destination/messages/01-content-removed/dest01_status new file mode 100644 index 0000000000..59586dad7f --- /dev/null +++ b/ci/tests/170-content-removal/channels/02-queued-destination/messages/01-content-removed/dest01_status @@ -0,0 +1 @@ +SENT \ No newline at end of file diff --git a/ci/tests/170-content-removal/channels/02-queued-destination/messages/01-content-removed/source b/ci/tests/170-content-removal/channels/02-queued-destination/messages/01-content-removed/source new file mode 100644 index 0000000000..6769dd60bd --- /dev/null +++ b/ci/tests/170-content-removal/channels/02-queued-destination/messages/01-content-removed/source @@ -0,0 +1 @@ +Hello world! \ No newline at end of file diff --git a/ci/tests/170-content-removal/channels/02-queued-destination/messages/01-content-removed/source_encoded b/ci/tests/170-content-removal/channels/02-queued-destination/messages/01-content-removed/source_encoded new file mode 100644 index 0000000000..20769dab04 --- /dev/null +++ b/ci/tests/170-content-removal/channels/02-queued-destination/messages/01-content-removed/source_encoded @@ -0,0 +1 @@ +((NONE)) \ No newline at end of file diff --git a/ci/tests/170-content-removal/channels/02-queued-destination/messages/01-content-removed/source_raw b/ci/tests/170-content-removal/channels/02-queued-destination/messages/01-content-removed/source_raw new file mode 100644 index 0000000000..20769dab04 --- /dev/null +++ b/ci/tests/170-content-removal/channels/02-queued-destination/messages/01-content-removed/source_raw @@ -0,0 +1 @@ +((NONE)) \ No newline at end of file diff --git a/ci/tests/170-content-removal/channels/02-queued-destination/messages/01-content-removed/source_status b/ci/tests/170-content-removal/channels/02-queued-destination/messages/01-content-removed/source_status new file mode 100644 index 0000000000..1316c06a69 --- /dev/null +++ b/ci/tests/170-content-removal/channels/02-queued-destination/messages/01-content-removed/source_status @@ -0,0 +1 @@ +TRANSFORMED \ No newline at end of file diff --git a/ci/tests/170-content-removal/channels/02-queued-destination/messages/01-content-removed/source_transformed b/ci/tests/170-content-removal/channels/02-queued-destination/messages/01-content-removed/source_transformed new file mode 100644 index 0000000000..20769dab04 --- /dev/null +++ b/ci/tests/170-content-removal/channels/02-queued-destination/messages/01-content-removed/source_transformed @@ -0,0 +1 @@ +((NONE)) \ No newline at end of file diff --git a/ci/tests/180-destination-chains/README.md b/ci/tests/180-destination-chains/README.md new file mode 100644 index 0000000000..f58b20c92b --- /dev/null +++ b/ci/tests/180-destination-chains/README.md @@ -0,0 +1,24 @@ +# 180-destination-chains + +Which maps a destination inherits, and from where. One RAW channel with four destinations; the third +has `waitForPrevious` false, so destinations 1-2 are one chain and destinations 3-4 another. The +source seeds `fromSource` in the channel and response maps, destination 1 adds `fromD1` to both, +destination 3 adds `fromD3` to the channel map, and each destination writes its own `cmN` to its +connector map. Every transformer renders what it can see *before* writing, so `destNN_transformed` +is a snapshot of what that destination inherited: + +| | d1 (chain A) | d2 (chain A) | d3 (chain B) | d4 (chain B) | +| --- | --- | --- | --- | --- | +| connector map `cm1` | absent | absent | absent | absent | +| channel map `fromSource` (`chS`) | S | S | S | S | +| channel map `fromD1` (`chD1`) | absent | **C1** | absent | absent | +| channel map `fromD3` (`chD3`) | absent | absent | absent | **C3** | +| response map `fromSource` (`rsS`) | present | present | present | present | +| response map `fromD1` (`rsD1`) | absent | **present** | absent | absent | + +Response map rows record only presence because the entry is stored as a serialised `Response`. The +bold cells are the only hand-off: channel and response maps pass to the next destination in the same +chain. Everything else comes from the source, because each chain starts afresh from the source +connector message, and `cm1` never propagates at all. + +`destNN_metadata.yml` asserts each `cmN` landed; `destNN_status` that both chains completed. diff --git a/ci/tests/180-destination-chains/channels/01-map-propagation/channel.xml b/ci/tests/180-destination-chains/channels/01-map-propagation/channel.xml new file mode 100644 index 0000000000..63d75a4f55 --- /dev/null +++ b/ci/tests/180-destination-chains/channels/01-map-propagation/channel.xml @@ -0,0 +1,472 @@ + + 5b9c7d21-3e64-4a58-9d0b-71c4f2ae1800 + 5 + destination-chains-map-propagation + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + + Seed channel and response maps + 0 + true + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + + Observe maps + 0 + true + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Writer + DESTINATION + true + true + + + 2 + Destination 2 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + + Observe maps + 0 + true + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Writer + DESTINATION + true + true + + + 3 + Destination 3 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + + Observe maps + 0 + true + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Writer + DESTINATION + true + false + + + 4 + Destination 4 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + + Observe maps + 0 + true + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest01_metadata.yml b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest01_metadata.yml new file mode 100644 index 0000000000..dde8b8aea6 --- /dev/null +++ b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest01_metadata.yml @@ -0,0 +1 @@ +cm1: v1 diff --git a/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest01_status b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest01_status new file mode 100644 index 0000000000..b09a037dd2 --- /dev/null +++ b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest01_status @@ -0,0 +1 @@ +SENT diff --git a/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest01_transformed b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest01_transformed new file mode 100644 index 0000000000..28dec358ae --- /dev/null +++ b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest01_transformed @@ -0,0 +1 @@ +d1|cm1=-|chS=S|chD1=-|chD3=-|rsS=Y|rsD1=- \ No newline at end of file diff --git a/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest02_metadata.yml b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest02_metadata.yml new file mode 100644 index 0000000000..ee6e5ff9dd --- /dev/null +++ b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest02_metadata.yml @@ -0,0 +1 @@ +cm2: v2 diff --git a/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest02_status b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest02_status new file mode 100644 index 0000000000..b09a037dd2 --- /dev/null +++ b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest02_status @@ -0,0 +1 @@ +SENT diff --git a/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest02_transformed b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest02_transformed new file mode 100644 index 0000000000..b623f23c1f --- /dev/null +++ b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest02_transformed @@ -0,0 +1 @@ +d2|cm1=-|chS=S|chD1=C1|chD3=-|rsS=Y|rsD1=Y \ No newline at end of file diff --git a/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest03_metadata.yml b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest03_metadata.yml new file mode 100644 index 0000000000..9996e604b7 --- /dev/null +++ b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest03_metadata.yml @@ -0,0 +1 @@ +cm3: v3 diff --git a/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest03_status b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest03_status new file mode 100644 index 0000000000..b09a037dd2 --- /dev/null +++ b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest03_status @@ -0,0 +1 @@ +SENT diff --git a/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest03_transformed b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest03_transformed new file mode 100644 index 0000000000..2899de4ece --- /dev/null +++ b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest03_transformed @@ -0,0 +1 @@ +d3|cm1=-|chS=S|chD1=-|chD3=-|rsS=Y|rsD1=- \ No newline at end of file diff --git a/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest04_metadata.yml b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest04_metadata.yml new file mode 100644 index 0000000000..7f47503033 --- /dev/null +++ b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest04_metadata.yml @@ -0,0 +1 @@ +cm4: v4 diff --git a/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest04_status b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest04_status new file mode 100644 index 0000000000..b09a037dd2 --- /dev/null +++ b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest04_status @@ -0,0 +1 @@ +SENT diff --git a/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest04_transformed b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest04_transformed new file mode 100644 index 0000000000..7acd204f9d --- /dev/null +++ b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/dest04_transformed @@ -0,0 +1 @@ +d4|cm1=-|chS=S|chD1=-|chD3=C3|rsS=Y|rsD1=- \ No newline at end of file diff --git a/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/source b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/source new file mode 100644 index 0000000000..739a197dc8 --- /dev/null +++ b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/source @@ -0,0 +1 @@ +Hello chains! \ No newline at end of file diff --git a/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/source_status b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/source_status new file mode 100644 index 0000000000..ddc7ed8516 --- /dev/null +++ b/ci/tests/180-destination-chains/channels/01-map-propagation/messages/01-maps-propagate/source_status @@ -0,0 +1 @@ +TRANSFORMED diff --git a/ci/tests/190-response-handling/README.md b/ci/tests/190-response-handling/README.md new file mode 100644 index 0000000000..3cc3346dcb --- /dev/null +++ b/ci/tests/190-response-handling/README.md @@ -0,0 +1,40 @@ +# 190-response-handling + +Where the response a channel hands back comes from, and what a destination's response transformer +can do to it. `Auto-generate (Destinations completed)` is covered by 110 and `Postprocessor` by 140, +so the channels here take the remaining "Respond from" choices plus the response transformer. + +- `01-respond-before-processing` - the 110 HL7 channel responding with + `Auto-generate (Before processing)` and a source filter that rejects ADT^A08. The one message is + an A08, so `source_status` is FILTERED while `source_response` is still an `MSA|AA` ACK: this + setting answers with an assumed RECEIVED status before the message is processed at all. +- `02-respond-after-source-transformer` - the same channel and filter responding with + `Auto-generate (After source transformer)`, which takes the source connector message's own status. + `01-accepted` is TRANSFORMED and gets `MSA|AA`; `02-filtered` is the same A08 as channel 01 and + gets `MSA|AR|Message Rejected.` from the rejected-ACK settings. The contrast with channel 01 is + the point of both channels. +- `03-respond-from-named-destination` - a RAW channel with two JavaScript Writers that answer + `first destination answered` and `second destination answered`, responding from `d2`. A named + destination's "Respond from" value is the response map key `d`, not the connector + name. `source_response` is destination 2's answer and `dest01_response` is destination 1's, so the + fixture would fail if the engine took the first, the last, or a merged response. +- `04-response-transformer` - a RAW channel whose one JavaScript Writer echoes the payload back, + behind a response transformer that rewrites the content and sets `responseStatus`. + - `01-rewrites-content-and-status` - `dest01_response` is the connector's original `rewrite-me`, + `dest01_processed_response` the rewritten `rewritten`, and `dest01_status` ERROR. + The send itself succeeded, so the status comes from the transformer alone. + - `02-transformer-error-fails-the-destination` - the payload `boom` makes the transformer throw. + The connector's response is still stored, no processed response is (`((NONE))`), the destination + is ERROR, and `dest01_processing_error` records the thrown message. + - `03-queued-status-without-a-queue` - the transformer asks for QUEUED on a destination whose + queue is off. The content is still rewritten, but the status is forced to ERROR and + `dest01_response_error` carries the engine's explanation. +- `05-respond-from-unknown-name` - the same RAW channel with one JavaScript Writer, responding from + `d9`, which no destination owns. The destination answers (`dest01_response`) but nothing is + selected, so `source_response` is `((NONE))`: an unresolvable name yields no response rather than + falling back to another one. + +The sixth case in this theme is a Java test, `smoketest` `BlockingResponseTransformerTest`: a +destination whose response transformer has not returned yet sits at PENDING with its sent content +already stored. The fixture harness retries until the message is terminal, so it cannot see that +status. Its channel lives in `smoketest/src/test/resources/channels/`. diff --git a/ci/tests/190-response-handling/channels/01-respond-before-processing/channel.xml b/ci/tests/190-response-handling/channels/01-respond-before-processing/channel.xml new file mode 100644 index 0000000000..27c36b4fbe --- /dev/null +++ b/ci/tests/190-response-handling/channels/01-respond-before-processing/channel.xml @@ -0,0 +1,392 @@ + + b1f0a6d2-3c4e-4a71-9d18-0e5b2c7a4411 + 2 + response-before-processing + + 1 + + 0 + sourceConnector + + + + Auto-generate (Before processing) + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + HL7V2 + HL7V2 + + + true + true + false + false + false + \r + true + + + false + false + \r + + + MSH_Segment + + + + \r + AA + + AE + An Error Occurred Processing Message. + AR + Message Rejected. + false + yyyyMMddHHmmss.SSS + + + AA,CA + AE,CE + AR,CR + true + Destination_Encoded + + + + + + true + true + false + false + false + \r + true + + + false + false + \r + + + MSH_Segment + + + + \r + AA + + AE + An Error Occurred Processing Message. + AR + Message Rejected. + false + yyyyMMddHHmmss.SSS + + + AA,CA + AE,CE + AR,CR + true + Destination_Encoded + + + + + + + + Reject A08 + 0 + true + NONE + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + HL7V2 + HL7V2 + + + true + true + false + false + false + \r + true + + + false + false + \r + + + MSH_Segment + + + + \r + AA + + AE + An Error Occurred Processing Message. + AR + Message Rejected. + false + yyyyMMddHHmmss.SSS + + + AA,CA + AE,CE + AR,CR + true + Destination_Encoded + + + + + + true + true + false + false + false + \r + true + + + false + false + \r + + + MSH_Segment + + + + \r + AA + + AE + An Error Occurred Processing Message. + AR + Message Rejected. + false + yyyyMMddHHmmss.SSS + + + AA,CA + AE,CE + AR,CR + true + Destination_Encoded + + + + + + + HL7V2 + HL7V2 + + + true + true + false + false + false + \r + true + + + false + false + \r + + + MSH_Segment + + + + \r + AA + + AE + An Error Occurred Processing Message. + AR + Message Rejected. + false + yyyyMMddHHmmss.SSS + + + AA,CA + AE,CE + AR,CR + true + Destination_Encoded + + + + + + true + true + false + false + false + \r + true + + + false + false + \r + + + MSH_Segment + + + + \r + AA + + AE + An Error Occurred Processing Message. + AR + Message Rejected. + false + yyyyMMddHHmmss.SSS + + + AA,CA + AE,CE + AR,CR + true + Destination_Encoded + + + + + + + + Channel Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/01-respond-before-processing/messages/01-filtered-still-acknowledged/source b/ci/tests/190-response-handling/channels/01-respond-before-processing/messages/01-filtered-still-acknowledged/source new file mode 100644 index 0000000000..9dea16ee69 --- /dev/null +++ b/ci/tests/190-response-handling/channels/01-respond-before-processing/messages/01-filtered-still-acknowledged/source @@ -0,0 +1,5 @@ +MSH|^~\&|Destination|Some Other App|Ehrsource|Or|202405221430||ADT^A08|Msgid88888|P|2.3| +EVN|A08|202405221430|| +PID|1|222333^^^2^MRN2|888777||Smith^Alice^^^^||19850412|F||Bl|456 Other St^^There^Or^97201^Usa||(503)555-1234|||S|Cat|111222333| +NK1|1|Smith^Robert^|Husband||(503)555-6789||||NK^Next Of Kin +PV1|1|I|3003^4004^02||||654321^Surgeon^Sarah^L^^Dr|||||||Emr|B1| diff --git a/ci/tests/190-response-handling/channels/01-respond-before-processing/messages/01-filtered-still-acknowledged/source_response b/ci/tests/190-response-handling/channels/01-respond-before-processing/messages/01-filtered-still-acknowledged/source_response new file mode 100644 index 0000000000..799b564e0c --- /dev/null +++ b/ci/tests/190-response-handling/channels/01-respond-before-processing/messages/01-filtered-still-acknowledged/source_response @@ -0,0 +1,2 @@ +MSH|^~\&|Ehrsource|Or|Destination|Some Other App|((ANY))||ACK|((ANY))|P|2.3 +MSA|AA|Msgid88888 diff --git a/ci/tests/190-response-handling/channels/01-respond-before-processing/messages/01-filtered-still-acknowledged/source_status b/ci/tests/190-response-handling/channels/01-respond-before-processing/messages/01-filtered-still-acknowledged/source_status new file mode 100644 index 0000000000..07e2ca686e --- /dev/null +++ b/ci/tests/190-response-handling/channels/01-respond-before-processing/messages/01-filtered-still-acknowledged/source_status @@ -0,0 +1 @@ +FILTERED \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/02-respond-after-source-transformer/channel.xml b/ci/tests/190-response-handling/channels/02-respond-after-source-transformer/channel.xml new file mode 100644 index 0000000000..0a1c86f0da --- /dev/null +++ b/ci/tests/190-response-handling/channels/02-respond-after-source-transformer/channel.xml @@ -0,0 +1,392 @@ + + c2a1b7e3-4d5f-4b82-8e29-1f6c3d8b5522 + 2 + response-after-source-transformer + + 1 + + 0 + sourceConnector + + + + Auto-generate (After source transformer) + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + HL7V2 + HL7V2 + + + true + true + false + false + false + \r + true + + + false + false + \r + + + MSH_Segment + + + + \r + AA + + AE + An Error Occurred Processing Message. + AR + Message Rejected. + false + yyyyMMddHHmmss.SSS + + + AA,CA + AE,CE + AR,CR + true + Destination_Encoded + + + + + + true + true + false + false + false + \r + true + + + false + false + \r + + + MSH_Segment + + + + \r + AA + + AE + An Error Occurred Processing Message. + AR + Message Rejected. + false + yyyyMMddHHmmss.SSS + + + AA,CA + AE,CE + AR,CR + true + Destination_Encoded + + + + + + + + Reject A08 + 0 + true + NONE + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + HL7V2 + HL7V2 + + + true + true + false + false + false + \r + true + + + false + false + \r + + + MSH_Segment + + + + \r + AA + + AE + An Error Occurred Processing Message. + AR + Message Rejected. + false + yyyyMMddHHmmss.SSS + + + AA,CA + AE,CE + AR,CR + true + Destination_Encoded + + + + + + true + true + false + false + false + \r + true + + + false + false + \r + + + MSH_Segment + + + + \r + AA + + AE + An Error Occurred Processing Message. + AR + Message Rejected. + false + yyyyMMddHHmmss.SSS + + + AA,CA + AE,CE + AR,CR + true + Destination_Encoded + + + + + + + HL7V2 + HL7V2 + + + true + true + false + false + false + \r + true + + + false + false + \r + + + MSH_Segment + + + + \r + AA + + AE + An Error Occurred Processing Message. + AR + Message Rejected. + false + yyyyMMddHHmmss.SSS + + + AA,CA + AE,CE + AR,CR + true + Destination_Encoded + + + + + + true + true + false + false + false + \r + true + + + false + false + \r + + + MSH_Segment + + + + \r + AA + + AE + An Error Occurred Processing Message. + AR + Message Rejected. + false + yyyyMMddHHmmss.SSS + + + AA,CA + AE,CE + AR,CR + true + Destination_Encoded + + + + + + + + Channel Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/02-respond-after-source-transformer/messages/01-accepted/source b/ci/tests/190-response-handling/channels/02-respond-after-source-transformer/messages/01-accepted/source new file mode 100644 index 0000000000..aa0a7adef1 --- /dev/null +++ b/ci/tests/190-response-handling/channels/02-respond-after-source-transformer/messages/01-accepted/source @@ -0,0 +1,5 @@ +MSH|^~\&|Destination|Some Other App|Ehrsource|Or|202405221430||ADT^A01|Msgid99999|P|2.3| +EVN|A01|202405221430|| +PID|1|222333^^^2^MRN2|888777||Smith^Alice^^^^||19850412|F||Bl|456 Other St^^There^Or^97201^Usa||(503)555-1234|||S|Cat|111222333| +NK1|1|Smith^Robert^|Husband||(503)555-6789||||NK^Next Of Kin +PV1|1|I|3003^4004^02||||654321^Surgeon^Sarah^L^^Dr|||||||Emr|B1| diff --git a/ci/tests/190-response-handling/channels/02-respond-after-source-transformer/messages/01-accepted/source_response b/ci/tests/190-response-handling/channels/02-respond-after-source-transformer/messages/01-accepted/source_response new file mode 100644 index 0000000000..d0124944af --- /dev/null +++ b/ci/tests/190-response-handling/channels/02-respond-after-source-transformer/messages/01-accepted/source_response @@ -0,0 +1,2 @@ +MSH|^~\&|Ehrsource|Or|Destination|Some Other App|((ANY))||ACK|((ANY))|P|2.3 +MSA|AA|Msgid99999 diff --git a/ci/tests/190-response-handling/channels/02-respond-after-source-transformer/messages/01-accepted/source_status b/ci/tests/190-response-handling/channels/02-respond-after-source-transformer/messages/01-accepted/source_status new file mode 100644 index 0000000000..1316c06a69 --- /dev/null +++ b/ci/tests/190-response-handling/channels/02-respond-after-source-transformer/messages/01-accepted/source_status @@ -0,0 +1 @@ +TRANSFORMED \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/02-respond-after-source-transformer/messages/02-filtered/source b/ci/tests/190-response-handling/channels/02-respond-after-source-transformer/messages/02-filtered/source new file mode 100644 index 0000000000..9dea16ee69 --- /dev/null +++ b/ci/tests/190-response-handling/channels/02-respond-after-source-transformer/messages/02-filtered/source @@ -0,0 +1,5 @@ +MSH|^~\&|Destination|Some Other App|Ehrsource|Or|202405221430||ADT^A08|Msgid88888|P|2.3| +EVN|A08|202405221430|| +PID|1|222333^^^2^MRN2|888777||Smith^Alice^^^^||19850412|F||Bl|456 Other St^^There^Or^97201^Usa||(503)555-1234|||S|Cat|111222333| +NK1|1|Smith^Robert^|Husband||(503)555-6789||||NK^Next Of Kin +PV1|1|I|3003^4004^02||||654321^Surgeon^Sarah^L^^Dr|||||||Emr|B1| diff --git a/ci/tests/190-response-handling/channels/02-respond-after-source-transformer/messages/02-filtered/source_response b/ci/tests/190-response-handling/channels/02-respond-after-source-transformer/messages/02-filtered/source_response new file mode 100644 index 0000000000..15eab366be --- /dev/null +++ b/ci/tests/190-response-handling/channels/02-respond-after-source-transformer/messages/02-filtered/source_response @@ -0,0 +1,2 @@ +MSH|^~\&|Ehrsource|Or|Destination|Some Other App|((ANY))||ACK|((ANY))|P|2.3 +MSA|AR|Msgid88888|Message Rejected. diff --git a/ci/tests/190-response-handling/channels/02-respond-after-source-transformer/messages/02-filtered/source_status b/ci/tests/190-response-handling/channels/02-respond-after-source-transformer/messages/02-filtered/source_status new file mode 100644 index 0000000000..07e2ca686e --- /dev/null +++ b/ci/tests/190-response-handling/channels/02-respond-after-source-transformer/messages/02-filtered/source_status @@ -0,0 +1 @@ +FILTERED \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/03-respond-from-named-destination/channel.xml b/ci/tests/190-response-handling/channels/03-respond-from-named-destination/channel.xml new file mode 100644 index 0000000000..bf801ff6a4 --- /dev/null +++ b/ci/tests/190-response-handling/channels/03-respond-from-named-destination/channel.xml @@ -0,0 +1,251 @@ + + d3b2c8f4-5e60-4c93-9f3a-206d4e9c6633 + 3 + response-from-named-destination + + 1 + + 0 + sourceConnector + + + + d2 + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + JavaScript Writer + DESTINATION + true + true + + + 2 + Destination 2 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + JavaScript Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/03-respond-from-named-destination/messages/01-second-destination-answers/dest01_response b/ci/tests/190-response-handling/channels/03-respond-from-named-destination/messages/01-second-destination-answers/dest01_response new file mode 100644 index 0000000000..6bf35c2da2 --- /dev/null +++ b/ci/tests/190-response-handling/channels/03-respond-from-named-destination/messages/01-second-destination-answers/dest01_response @@ -0,0 +1 @@ +first destination answered \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/03-respond-from-named-destination/messages/01-second-destination-answers/dest02_response b/ci/tests/190-response-handling/channels/03-respond-from-named-destination/messages/01-second-destination-answers/dest02_response new file mode 100644 index 0000000000..5f82bc5836 --- /dev/null +++ b/ci/tests/190-response-handling/channels/03-respond-from-named-destination/messages/01-second-destination-answers/dest02_response @@ -0,0 +1 @@ +second destination answered \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/03-respond-from-named-destination/messages/01-second-destination-answers/source b/ci/tests/190-response-handling/channels/03-respond-from-named-destination/messages/01-second-destination-answers/source new file mode 100644 index 0000000000..518f936d6d --- /dev/null +++ b/ci/tests/190-response-handling/channels/03-respond-from-named-destination/messages/01-second-destination-answers/source @@ -0,0 +1 @@ +pick-the-named-destination \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/03-respond-from-named-destination/messages/01-second-destination-answers/source_response b/ci/tests/190-response-handling/channels/03-respond-from-named-destination/messages/01-second-destination-answers/source_response new file mode 100644 index 0000000000..5f82bc5836 --- /dev/null +++ b/ci/tests/190-response-handling/channels/03-respond-from-named-destination/messages/01-second-destination-answers/source_response @@ -0,0 +1 @@ +second destination answered \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/04-response-transformer/channel.xml b/ci/tests/190-response-handling/channels/04-response-transformer/channel.xml new file mode 100644 index 0000000000..ece279b6da --- /dev/null +++ b/ci/tests/190-response-handling/channels/04-response-transformer/channel.xml @@ -0,0 +1,195 @@ + + e4c3d9a5-6f71-4da4-8a4b-317e5f0d7744 + 2 + response-transformer + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Rewrite the response + 0 + true + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + JavaScript Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/04-response-transformer/messages/01-rewrites-content-and-status/dest01_processed_response b/ci/tests/190-response-handling/channels/04-response-transformer/messages/01-rewrites-content-and-status/dest01_processed_response new file mode 100644 index 0000000000..100ed092d4 --- /dev/null +++ b/ci/tests/190-response-handling/channels/04-response-transformer/messages/01-rewrites-content-and-status/dest01_processed_response @@ -0,0 +1 @@ +rewritten \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/04-response-transformer/messages/01-rewrites-content-and-status/dest01_response b/ci/tests/190-response-handling/channels/04-response-transformer/messages/01-rewrites-content-and-status/dest01_response new file mode 100644 index 0000000000..537a9eaf7e --- /dev/null +++ b/ci/tests/190-response-handling/channels/04-response-transformer/messages/01-rewrites-content-and-status/dest01_response @@ -0,0 +1 @@ +rewrite-me \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/04-response-transformer/messages/01-rewrites-content-and-status/dest01_status b/ci/tests/190-response-handling/channels/04-response-transformer/messages/01-rewrites-content-and-status/dest01_status new file mode 100644 index 0000000000..17416ae33e --- /dev/null +++ b/ci/tests/190-response-handling/channels/04-response-transformer/messages/01-rewrites-content-and-status/dest01_status @@ -0,0 +1 @@ +ERROR \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/04-response-transformer/messages/01-rewrites-content-and-status/source b/ci/tests/190-response-handling/channels/04-response-transformer/messages/01-rewrites-content-and-status/source new file mode 100644 index 0000000000..537a9eaf7e --- /dev/null +++ b/ci/tests/190-response-handling/channels/04-response-transformer/messages/01-rewrites-content-and-status/source @@ -0,0 +1 @@ +rewrite-me \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/04-response-transformer/messages/02-transformer-error-fails-the-destination/dest01_processed_response b/ci/tests/190-response-handling/channels/04-response-transformer/messages/02-transformer-error-fails-the-destination/dest01_processed_response new file mode 100644 index 0000000000..20769dab04 --- /dev/null +++ b/ci/tests/190-response-handling/channels/04-response-transformer/messages/02-transformer-error-fails-the-destination/dest01_processed_response @@ -0,0 +1 @@ +((NONE)) \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/04-response-transformer/messages/02-transformer-error-fails-the-destination/dest01_processing_error b/ci/tests/190-response-handling/channels/04-response-transformer/messages/02-transformer-error-fails-the-destination/dest01_processing_error new file mode 100644 index 0000000000..3c6126e58f --- /dev/null +++ b/ci/tests/190-response-handling/channels/04-response-transformer/messages/02-transformer-error-fails-the-destination/dest01_processing_error @@ -0,0 +1,4 @@ +Response Transformer error +ERROR MESSAGE: Error evaluating response transformer +((ANY))DETAILS: the response transformer refused this response +((ANY)) \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/04-response-transformer/messages/02-transformer-error-fails-the-destination/dest01_response b/ci/tests/190-response-handling/channels/04-response-transformer/messages/02-transformer-error-fails-the-destination/dest01_response new file mode 100644 index 0000000000..2e166c8e94 --- /dev/null +++ b/ci/tests/190-response-handling/channels/04-response-transformer/messages/02-transformer-error-fails-the-destination/dest01_response @@ -0,0 +1 @@ +boom \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/04-response-transformer/messages/02-transformer-error-fails-the-destination/dest01_status b/ci/tests/190-response-handling/channels/04-response-transformer/messages/02-transformer-error-fails-the-destination/dest01_status new file mode 100644 index 0000000000..17416ae33e --- /dev/null +++ b/ci/tests/190-response-handling/channels/04-response-transformer/messages/02-transformer-error-fails-the-destination/dest01_status @@ -0,0 +1 @@ +ERROR \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/04-response-transformer/messages/02-transformer-error-fails-the-destination/source b/ci/tests/190-response-handling/channels/04-response-transformer/messages/02-transformer-error-fails-the-destination/source new file mode 100644 index 0000000000..2e166c8e94 --- /dev/null +++ b/ci/tests/190-response-handling/channels/04-response-transformer/messages/02-transformer-error-fails-the-destination/source @@ -0,0 +1 @@ +boom \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/04-response-transformer/messages/03-queued-status-without-a-queue/dest01_processed_response b/ci/tests/190-response-handling/channels/04-response-transformer/messages/03-queued-status-without-a-queue/dest01_processed_response new file mode 100644 index 0000000000..68601d9bdb --- /dev/null +++ b/ci/tests/190-response-handling/channels/04-response-transformer/messages/03-queued-status-without-a-queue/dest01_processed_response @@ -0,0 +1 @@ +rewritten \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/04-response-transformer/messages/03-queued-status-without-a-queue/dest01_response_error b/ci/tests/190-response-handling/channels/04-response-transformer/messages/03-queued-status-without-a-queue/dest01_response_error new file mode 100644 index 0000000000..81975b705d --- /dev/null +++ b/ci/tests/190-response-handling/channels/04-response-transformer/messages/03-queued-status-without-a-queue/dest01_response_error @@ -0,0 +1 @@ +Invalid response status. Cannot set status to QUEUED while queuing is disabled. Status updated to ERROR. \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/04-response-transformer/messages/03-queued-status-without-a-queue/dest01_status b/ci/tests/190-response-handling/channels/04-response-transformer/messages/03-queued-status-without-a-queue/dest01_status new file mode 100644 index 0000000000..17416ae33e --- /dev/null +++ b/ci/tests/190-response-handling/channels/04-response-transformer/messages/03-queued-status-without-a-queue/dest01_status @@ -0,0 +1 @@ +ERROR \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/04-response-transformer/messages/03-queued-status-without-a-queue/source b/ci/tests/190-response-handling/channels/04-response-transformer/messages/03-queued-status-without-a-queue/source new file mode 100644 index 0000000000..3f8cd74914 --- /dev/null +++ b/ci/tests/190-response-handling/channels/04-response-transformer/messages/03-queued-status-without-a-queue/source @@ -0,0 +1 @@ +queue-me \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/05-respond-from-unknown-name/channel.xml b/ci/tests/190-response-handling/channels/05-respond-from-unknown-name/channel.xml new file mode 100644 index 0000000000..deb6ae0bed --- /dev/null +++ b/ci/tests/190-response-handling/channels/05-respond-from-unknown-name/channel.xml @@ -0,0 +1,182 @@ + + f5d4ea16-7082-4eb5-9b5c-428f601e8855 + 2 + response-from-unknown-name + + 1 + + 0 + sourceConnector + + + + d9 + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + JavaScript Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/05-respond-from-unknown-name/messages/01-no-response-is-stored/dest01_response b/ci/tests/190-response-handling/channels/05-respond-from-unknown-name/messages/01-no-response-is-stored/dest01_response new file mode 100644 index 0000000000..471e51e821 --- /dev/null +++ b/ci/tests/190-response-handling/channels/05-respond-from-unknown-name/messages/01-no-response-is-stored/dest01_response @@ -0,0 +1 @@ +the destination answered \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/05-respond-from-unknown-name/messages/01-no-response-is-stored/source b/ci/tests/190-response-handling/channels/05-respond-from-unknown-name/messages/01-no-response-is-stored/source new file mode 100644 index 0000000000..7e77c51412 --- /dev/null +++ b/ci/tests/190-response-handling/channels/05-respond-from-unknown-name/messages/01-no-response-is-stored/source @@ -0,0 +1 @@ +no-one-to-respond-from \ No newline at end of file diff --git a/ci/tests/190-response-handling/channels/05-respond-from-unknown-name/messages/01-no-response-is-stored/source_response b/ci/tests/190-response-handling/channels/05-respond-from-unknown-name/messages/01-no-response-is-stored/source_response new file mode 100644 index 0000000000..20769dab04 --- /dev/null +++ b/ci/tests/190-response-handling/channels/05-respond-from-unknown-name/messages/01-no-response-is-stored/source_response @@ -0,0 +1 @@ +((NONE)) \ No newline at end of file diff --git a/ci/tests/200-custom-metadata-columns/README.md b/ci/tests/200-custom-metadata-columns/README.md new file mode 100644 index 0000000000..64c1bbb34b --- /dev/null +++ b/ci/tests/200-custom-metadata-columns/README.md @@ -0,0 +1,34 @@ +# 200-custom-metadata-columns + +Custom metadata columns: a channel declares them, the engine adds a real database column per +declared column on deploy, and each connector message gets its own row of values. One RAW channel +with four columns, one per `MetaDataColumnType`, each mapped to a lower-case source map key: + +| column | type | mapping name | +| --- | --- | --- | +| `MDSTRING` | STRING | `mdstring` | +| `MDNUMBER` | NUMBER | `mdnumber` | +| `MDBOOLEAN` | BOOLEAN | `mdboolean` | +| `MDTIMESTAMP` | TIMESTAMP | `mdtimestamp` | + +Values arrive in the source map rather than from a transformer script, so each message is nothing +but the values in and the stored values out. `source_metadata.yml` asserts what came back out of +the database, keyed by column name — never by mapping name, which would also match the connector +map and prove nothing about storage. A NUMBER column is `DECIMAL(31, 15)` in every dialect, so its +values come back with fifteen decimal places. + +- `01-all-types` - one value per type survives the round trip. Destination 1's transformer puts its + own `mdstring` in the connector map, which the replacer prefers over the source map, so + `dest01_metadata.yml` shows the destination's row holding a different string from the source's + while its number is unchanged. Two rows, not one copied. +- `02-value-casting` - the same four columns fed values that are not already of the column's type: + `YES` becomes the boolean `true`, an HL7-style `20100102130102` becomes the same instant as + `01-all-types`' `2010-01-02 13:01:02`, and an over-long string is truncated to 255 characters, + which is where the `|255|` marker in the expected value sits. +- `03-absent-and-uncastable` - a column with nothing mapped to it, and a NUMBER column fed `not a + number`, both end up with no value. Casting is best-effort: the failure is logged and the message + still reaches TRANSFORMED, which `source_status` asserts. + +Adding a column is all a fixture can reach, because it deploys each channel exactly once. Removing +a column, and the drop-then-add that changing a column's type compiles to, need a channel that is +edited and redeployed, which is `CustomMetaDataColumnRedeployTest`. diff --git a/ci/tests/200-custom-metadata-columns/channels/01-column-types/channel.xml b/ci/tests/200-custom-metadata-columns/channels/01-column-types/channel.xml new file mode 100644 index 0000000000..6d4bb4a885 --- /dev/null +++ b/ci/tests/200-custom-metadata-columns/channels/01-column-types/channel.xml @@ -0,0 +1,201 @@ + + 7c1f0a94-2d63-4b85-9e17-0a5d3b6c2001 + 2 + custom-metadata-column-types + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + + Override the string mapping for this destination + 0 + true + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + MDSTRING + STRING + mdstring + + + MDNUMBER + NUMBER + mdnumber + + + MDBOOLEAN + BOOLEAN + mdboolean + + + MDTIMESTAMP + TIMESTAMP + mdtimestamp + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/01-all-types/dest01_metadata.yml b/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/01-all-types/dest01_metadata.yml new file mode 100644 index 0000000000..b214400fca --- /dev/null +++ b/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/01-all-types/dest01_metadata.yml @@ -0,0 +1,2 @@ +MDSTRING: "set by destination 1" +MDNUMBER: "1234.5678" diff --git a/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/01-all-types/source b/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/01-all-types/source new file mode 100644 index 0000000000..6769dd60bd --- /dev/null +++ b/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/01-all-types/source @@ -0,0 +1 @@ +Hello world! \ No newline at end of file diff --git a/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/01-all-types/source_metadata.yml b/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/01-all-types/source_metadata.yml new file mode 100644 index 0000000000..7a76df7b7a --- /dev/null +++ b/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/01-all-types/source_metadata.yml @@ -0,0 +1,4 @@ +MDSTRING: "a string value" +MDNUMBER: "1234.5678" +MDBOOLEAN: "true" +MDTIMESTAMP: "2010-01-02T13:01:02Z" diff --git a/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/01-all-types/source_sourcemap.yml b/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/01-all-types/source_sourcemap.yml new file mode 100644 index 0000000000..c78bb0aeb7 --- /dev/null +++ b/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/01-all-types/source_sourcemap.yml @@ -0,0 +1,4 @@ +mdstring: "a string value" +mdnumber: "1234.5678" +mdboolean: "true" +mdtimestamp: "2010-01-02 13:01:02" diff --git a/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/02-value-casting/source b/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/02-value-casting/source new file mode 100644 index 0000000000..6769dd60bd --- /dev/null +++ b/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/02-value-casting/source @@ -0,0 +1 @@ +Hello world! \ No newline at end of file diff --git a/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/02-value-casting/source_metadata.yml b/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/02-value-casting/source_metadata.yml new file mode 100644 index 0000000000..b374b13ae0 --- /dev/null +++ b/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/02-value-casting/source_metadata.yml @@ -0,0 +1,4 @@ +MDSTRING: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|255|" +MDNUMBER: "-3.5" +MDBOOLEAN: "true" +MDTIMESTAMP: "2010-01-02T13:01:02Z" diff --git a/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/02-value-casting/source_sourcemap.yml b/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/02-value-casting/source_sourcemap.yml new file mode 100644 index 0000000000..ead4412633 --- /dev/null +++ b/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/02-value-casting/source_sourcemap.yml @@ -0,0 +1,4 @@ +mdstring: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|255|this tail is past the 255 character limit" +mdnumber: "-3.5" +mdboolean: "YES" +mdtimestamp: "20100102130102" diff --git a/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/03-absent-and-uncastable/source b/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/03-absent-and-uncastable/source new file mode 100644 index 0000000000..6769dd60bd --- /dev/null +++ b/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/03-absent-and-uncastable/source @@ -0,0 +1 @@ +Hello world! \ No newline at end of file diff --git a/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/03-absent-and-uncastable/source_metadata.yml b/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/03-absent-and-uncastable/source_metadata.yml new file mode 100644 index 0000000000..46b1d49c4e --- /dev/null +++ b/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/03-absent-and-uncastable/source_metadata.yml @@ -0,0 +1,4 @@ +MDSTRING: "((NONE))" +MDNUMBER: "((NONE))" +MDTIMESTAMP: "((NONE))" +MDBOOLEAN: "true" diff --git a/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/03-absent-and-uncastable/source_sourcemap.yml b/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/03-absent-and-uncastable/source_sourcemap.yml new file mode 100644 index 0000000000..a058ae0b87 --- /dev/null +++ b/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/03-absent-and-uncastable/source_sourcemap.yml @@ -0,0 +1,2 @@ +mdnumber: "not a number" +mdboolean: "true" diff --git a/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/03-absent-and-uncastable/source_status b/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/03-absent-and-uncastable/source_status new file mode 100644 index 0000000000..1316c06a69 --- /dev/null +++ b/ci/tests/200-custom-metadata-columns/channels/01-column-types/messages/03-absent-and-uncastable/source_status @@ -0,0 +1 @@ +TRANSFORMED \ No newline at end of file diff --git a/ci/tests/230-attachments/README.md b/ci/tests/230-attachments/README.md new file mode 100644 index 0000000000..8d13974dcc --- /dev/null +++ b/ci/tests/230-attachments/README.md @@ -0,0 +1,25 @@ +# 230-attachments + +Attachment storage and retrieval alongside the message. All three channels use the same Regex +attachment handler, whose two patterns pull the value out of `PHOTO:...;` as `image/jpeg` and out +of `REPORT:...;` as `text/plain`, leaving a `${ATTACH:id}` token in the message where each one was. +The payload carries a non-ASCII character so that the attachment content is not the same bytes as +its characters, which is what makes `attachmentNN`'s byte-for-byte comparison worth having. + +- `01-regex-extraction` - extraction and storage: the message keeps the tokens, the attachments keep + the content and the mime type of the pattern that matched. + - `01-two-attachments` - both patterns fire. `source_raw` carries a token in place of each + extracted value, `attachment01`/`attachment02` are what was taken out, in the order their tokens + appear, and `attachment03` is `((NONE))`: exactly two attachments were stored, not three. + - `02-blank-attachment-not-extracted` - `PHOTO:;` matches the pattern with an empty value. The + handler leaves a blank match in the message rather than storing an empty attachment, so + `source_raw` still reads `PHOTO:;` and the report is the only attachment. +- `02-reattachment` - retrieval: the same channel with a source transformer step calling + `AttachmentUtil.reAttachMessage(connectorMessage)`. `source_raw` is the tokenised message and + `source_transformed` is the original payload, so the engine read both attachments back out of + storage by ID and put them where their tokens were. This is the read a destination connector makes + when `reattachAttachments` is set. +- `03-remove-on-completion` - the same channel with `removeAttachmentsOnCompletion`. Once the message + completes, `attachment01` is `((NONE))` while `source_raw` still holds the tokens: the attachments + are dropped and the message is not, which is what makes the setting lossy. Content removal is off, + so this is the attachment half of 170-content-removal on its own. diff --git a/ci/tests/230-attachments/channels/01-regex-extraction/channel.xml b/ci/tests/230-attachments/channels/01-regex-extraction/channel.xml new file mode 100644 index 0000000000..6e28d5507b --- /dev/null +++ b/ci/tests/230-attachments/channels/01-regex-extraction/channel.xml @@ -0,0 +1,202 @@ + + a1d0f2c4-3b5e-4a07-9c11-2e6d7f480001 + 2 + Attachments Regex Extraction + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + com.mirth.connect.server.attachments.regex.RegexAttachmentHandlerProvider + Regex + + + regex.pattern0 + PHOTO:(.*?); + + + regex.mimetype0 + image/jpeg + + + regex.pattern1 + REPORT:(.*?); + + + regex.mimetype1 + text/plain + + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/ci/tests/230-attachments/channels/01-regex-extraction/messages/01-two-attachments/attachment01 b/ci/tests/230-attachments/channels/01-regex-extraction/messages/01-two-attachments/attachment01 new file mode 100644 index 0000000000..eb86b696b8 --- /dev/null +++ b/ci/tests/230-attachments/channels/01-regex-extraction/messages/01-two-attachments/attachment01 @@ -0,0 +1 @@ +PHOTO-CONTENT-café \ No newline at end of file diff --git a/ci/tests/230-attachments/channels/01-regex-extraction/messages/01-two-attachments/attachment01_type b/ci/tests/230-attachments/channels/01-regex-extraction/messages/01-two-attachments/attachment01_type new file mode 100644 index 0000000000..29e455a21e --- /dev/null +++ b/ci/tests/230-attachments/channels/01-regex-extraction/messages/01-two-attachments/attachment01_type @@ -0,0 +1 @@ +image/jpeg \ No newline at end of file diff --git a/ci/tests/230-attachments/channels/01-regex-extraction/messages/01-two-attachments/attachment02 b/ci/tests/230-attachments/channels/01-regex-extraction/messages/01-two-attachments/attachment02 new file mode 100644 index 0000000000..b02bbeb98f --- /dev/null +++ b/ci/tests/230-attachments/channels/01-regex-extraction/messages/01-two-attachments/attachment02 @@ -0,0 +1 @@ +REPORT-CONTENT-TWO \ No newline at end of file diff --git a/ci/tests/230-attachments/channels/01-regex-extraction/messages/01-two-attachments/attachment02_type b/ci/tests/230-attachments/channels/01-regex-extraction/messages/01-two-attachments/attachment02_type new file mode 100644 index 0000000000..f1148cc2ba --- /dev/null +++ b/ci/tests/230-attachments/channels/01-regex-extraction/messages/01-two-attachments/attachment02_type @@ -0,0 +1 @@ +text/plain \ No newline at end of file diff --git a/ci/tests/230-attachments/channels/01-regex-extraction/messages/01-two-attachments/attachment03 b/ci/tests/230-attachments/channels/01-regex-extraction/messages/01-two-attachments/attachment03 new file mode 100644 index 0000000000..20769dab04 --- /dev/null +++ b/ci/tests/230-attachments/channels/01-regex-extraction/messages/01-two-attachments/attachment03 @@ -0,0 +1 @@ +((NONE)) \ No newline at end of file diff --git a/ci/tests/230-attachments/channels/01-regex-extraction/messages/01-two-attachments/source b/ci/tests/230-attachments/channels/01-regex-extraction/messages/01-two-attachments/source new file mode 100644 index 0000000000..a747530273 --- /dev/null +++ b/ci/tests/230-attachments/channels/01-regex-extraction/messages/01-two-attachments/source @@ -0,0 +1,4 @@ +patient=12345 +PHOTO:PHOTO-CONTENT-café; +REPORT:REPORT-CONTENT-TWO; +end \ No newline at end of file diff --git a/ci/tests/230-attachments/channels/01-regex-extraction/messages/01-two-attachments/source_raw b/ci/tests/230-attachments/channels/01-regex-extraction/messages/01-two-attachments/source_raw new file mode 100644 index 0000000000..1086e00735 --- /dev/null +++ b/ci/tests/230-attachments/channels/01-regex-extraction/messages/01-two-attachments/source_raw @@ -0,0 +1,4 @@ +patient=12345 +PHOTO:${ATTACH:((ANY))}; +REPORT:${ATTACH:((ANY))}; +end \ No newline at end of file diff --git a/ci/tests/230-attachments/channels/01-regex-extraction/messages/02-blank-attachment-not-extracted/attachment01 b/ci/tests/230-attachments/channels/01-regex-extraction/messages/02-blank-attachment-not-extracted/attachment01 new file mode 100644 index 0000000000..22fd6e87ab --- /dev/null +++ b/ci/tests/230-attachments/channels/01-regex-extraction/messages/02-blank-attachment-not-extracted/attachment01 @@ -0,0 +1 @@ +REPORT-ONLY \ No newline at end of file diff --git a/ci/tests/230-attachments/channels/01-regex-extraction/messages/02-blank-attachment-not-extracted/attachment01_type b/ci/tests/230-attachments/channels/01-regex-extraction/messages/02-blank-attachment-not-extracted/attachment01_type new file mode 100644 index 0000000000..f1148cc2ba --- /dev/null +++ b/ci/tests/230-attachments/channels/01-regex-extraction/messages/02-blank-attachment-not-extracted/attachment01_type @@ -0,0 +1 @@ +text/plain \ No newline at end of file diff --git a/ci/tests/230-attachments/channels/01-regex-extraction/messages/02-blank-attachment-not-extracted/attachment02 b/ci/tests/230-attachments/channels/01-regex-extraction/messages/02-blank-attachment-not-extracted/attachment02 new file mode 100644 index 0000000000..20769dab04 --- /dev/null +++ b/ci/tests/230-attachments/channels/01-regex-extraction/messages/02-blank-attachment-not-extracted/attachment02 @@ -0,0 +1 @@ +((NONE)) \ No newline at end of file diff --git a/ci/tests/230-attachments/channels/01-regex-extraction/messages/02-blank-attachment-not-extracted/source b/ci/tests/230-attachments/channels/01-regex-extraction/messages/02-blank-attachment-not-extracted/source new file mode 100644 index 0000000000..e9cb0d8bf0 --- /dev/null +++ b/ci/tests/230-attachments/channels/01-regex-extraction/messages/02-blank-attachment-not-extracted/source @@ -0,0 +1,4 @@ +patient=12345 +PHOTO:; +REPORT:REPORT-ONLY; +end \ No newline at end of file diff --git a/ci/tests/230-attachments/channels/01-regex-extraction/messages/02-blank-attachment-not-extracted/source_raw b/ci/tests/230-attachments/channels/01-regex-extraction/messages/02-blank-attachment-not-extracted/source_raw new file mode 100644 index 0000000000..9ee13257f4 --- /dev/null +++ b/ci/tests/230-attachments/channels/01-regex-extraction/messages/02-blank-attachment-not-extracted/source_raw @@ -0,0 +1,4 @@ +patient=12345 +PHOTO:; +REPORT:${ATTACH:((ANY))}; +end \ No newline at end of file diff --git a/ci/tests/230-attachments/channels/02-reattachment/channel.xml b/ci/tests/230-attachments/channels/02-reattachment/channel.xml new file mode 100644 index 0000000000..6e898e31db --- /dev/null +++ b/ci/tests/230-attachments/channels/02-reattachment/channel.xml @@ -0,0 +1,209 @@ + + a1d0f2c4-3b5e-4a07-9c11-2e6d7f480002 + 2 + Attachments Reattachment + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + + Re-attach + 0 + true + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + com.mirth.connect.server.attachments.regex.RegexAttachmentHandlerProvider + Regex + + + regex.pattern0 + PHOTO:(.*?); + + + regex.mimetype0 + image/jpeg + + + regex.pattern1 + REPORT:(.*?); + + + regex.mimetype1 + text/plain + + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/ci/tests/230-attachments/channels/02-reattachment/messages/01-restored-in-transformer/source b/ci/tests/230-attachments/channels/02-reattachment/messages/01-restored-in-transformer/source new file mode 100644 index 0000000000..a747530273 --- /dev/null +++ b/ci/tests/230-attachments/channels/02-reattachment/messages/01-restored-in-transformer/source @@ -0,0 +1,4 @@ +patient=12345 +PHOTO:PHOTO-CONTENT-café; +REPORT:REPORT-CONTENT-TWO; +end \ No newline at end of file diff --git a/ci/tests/230-attachments/channels/02-reattachment/messages/01-restored-in-transformer/source_raw b/ci/tests/230-attachments/channels/02-reattachment/messages/01-restored-in-transformer/source_raw new file mode 100644 index 0000000000..1086e00735 --- /dev/null +++ b/ci/tests/230-attachments/channels/02-reattachment/messages/01-restored-in-transformer/source_raw @@ -0,0 +1,4 @@ +patient=12345 +PHOTO:${ATTACH:((ANY))}; +REPORT:${ATTACH:((ANY))}; +end \ No newline at end of file diff --git a/ci/tests/230-attachments/channels/02-reattachment/messages/01-restored-in-transformer/source_transformed b/ci/tests/230-attachments/channels/02-reattachment/messages/01-restored-in-transformer/source_transformed new file mode 100644 index 0000000000..a747530273 --- /dev/null +++ b/ci/tests/230-attachments/channels/02-reattachment/messages/01-restored-in-transformer/source_transformed @@ -0,0 +1,4 @@ +patient=12345 +PHOTO:PHOTO-CONTENT-café; +REPORT:REPORT-CONTENT-TWO; +end \ No newline at end of file diff --git a/ci/tests/230-attachments/channels/03-remove-on-completion/channel.xml b/ci/tests/230-attachments/channels/03-remove-on-completion/channel.xml new file mode 100644 index 0000000000..9d9c1b994f --- /dev/null +++ b/ci/tests/230-attachments/channels/03-remove-on-completion/channel.xml @@ -0,0 +1,202 @@ + + a1d0f2c4-3b5e-4a07-9c11-2e6d7f480004 + 2 + Attachments Remove On Completion + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + false + false + true + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + com.mirth.connect.server.attachments.regex.RegexAttachmentHandlerProvider + Regex + + + regex.pattern0 + PHOTO:(.*?); + + + regex.mimetype0 + image/jpeg + + + regex.pattern1 + REPORT:(.*?); + + + regex.mimetype1 + text/plain + + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/ci/tests/230-attachments/channels/03-remove-on-completion/messages/01-attachments-dropped-tokens-kept/attachment01 b/ci/tests/230-attachments/channels/03-remove-on-completion/messages/01-attachments-dropped-tokens-kept/attachment01 new file mode 100644 index 0000000000..20769dab04 --- /dev/null +++ b/ci/tests/230-attachments/channels/03-remove-on-completion/messages/01-attachments-dropped-tokens-kept/attachment01 @@ -0,0 +1 @@ +((NONE)) \ No newline at end of file diff --git a/ci/tests/230-attachments/channels/03-remove-on-completion/messages/01-attachments-dropped-tokens-kept/source b/ci/tests/230-attachments/channels/03-remove-on-completion/messages/01-attachments-dropped-tokens-kept/source new file mode 100644 index 0000000000..a747530273 --- /dev/null +++ b/ci/tests/230-attachments/channels/03-remove-on-completion/messages/01-attachments-dropped-tokens-kept/source @@ -0,0 +1,4 @@ +patient=12345 +PHOTO:PHOTO-CONTENT-café; +REPORT:REPORT-CONTENT-TWO; +end \ No newline at end of file diff --git a/ci/tests/230-attachments/channels/03-remove-on-completion/messages/01-attachments-dropped-tokens-kept/source_raw b/ci/tests/230-attachments/channels/03-remove-on-completion/messages/01-attachments-dropped-tokens-kept/source_raw new file mode 100644 index 0000000000..1086e00735 --- /dev/null +++ b/ci/tests/230-attachments/channels/03-remove-on-completion/messages/01-attachments-dropped-tokens-kept/source_raw @@ -0,0 +1,4 @@ +patient=12345 +PHOTO:${ATTACH:((ANY))}; +REPORT:${ATTACH:((ANY))}; +end \ No newline at end of file diff --git a/donkey/src/main/java/com/mirth/connect/donkey/server/controllers/ChannelController.java b/donkey/src/main/java/com/mirth/connect/donkey/server/controllers/ChannelController.java index c3b219087a..adac29ec2c 100644 --- a/donkey/src/main/java/com/mirth/connect/donkey/server/controllers/ChannelController.java +++ b/donkey/src/main/java/com/mirth/connect/donkey/server/controllers/ChannelController.java @@ -86,10 +86,18 @@ public Statistics getTotalStatistics() { } public Statistics getStatisticsFromStorage(String serverId) { + return getStatisticsFromStorage(serverId, null); + } + + /** + * The stored current statistics of the given channels, or of every channel when + * {@code channelIds} is null or empty. + */ + public Statistics getStatisticsFromStorage(String serverId, Set channelIds) { DonkeyDao dao = donkey.getReadOnlyDaoFactory().getDao(); try { - return dao.getChannelStatistics(serverId); + return dao.getChannelStatistics(serverId, channelIds); } finally { dao.close(); } @@ -106,10 +114,15 @@ public List getPortsInUse() { } public Statistics getTotalStatisticsFromStorage(String serverId) { + return getTotalStatisticsFromStorage(serverId, null); + } + + /** The lifetime counterpart of {@link #getStatisticsFromStorage(String, Set)}. */ + public Statistics getTotalStatisticsFromStorage(String serverId, Set channelIds) { DonkeyDao dao = donkey.getReadOnlyDaoFactory().getDao(); try { - return dao.getChannelTotalStatistics(serverId); + return dao.getChannelTotalStatistics(serverId, channelIds); } finally { dao.close(); } diff --git a/donkey/src/main/java/com/mirth/connect/donkey/server/data/DonkeyDao.java b/donkey/src/main/java/com/mirth/connect/donkey/server/data/DonkeyDao.java index 032d65986e..320b7f2d60 100644 --- a/donkey/src/main/java/com/mirth/connect/donkey/server/data/DonkeyDao.java +++ b/donkey/src/main/java/com/mirth/connect/donkey/server/data/DonkeyDao.java @@ -130,7 +130,13 @@ public interface DonkeyDao { public Statistics getChannelStatistics(String serverId); + /** The current statistics of the given channels only. A null or empty set means every channel. */ + public Statistics getChannelStatistics(String serverId, Set channelIds); + public Statistics getChannelTotalStatistics(String serverId); + + /** The lifetime counterpart of {@link #getChannelStatistics(String, Set)}. */ + public Statistics getChannelTotalStatistics(String serverId, Set channelIds); public List getPortsInUse(); diff --git a/donkey/src/main/java/com/mirth/connect/donkey/server/data/buffered/BufferedDao.java b/donkey/src/main/java/com/mirth/connect/donkey/server/data/buffered/BufferedDao.java index fc1c2efbaa..001aa93a2e 100644 --- a/donkey/src/main/java/com/mirth/connect/donkey/server/data/buffered/BufferedDao.java +++ b/donkey/src/main/java/com/mirth/connect/donkey/server/data/buffered/BufferedDao.java @@ -579,6 +579,17 @@ public Statistics getChannelStatistics(String serverId) { } } + @Override + public Statistics getChannelStatistics(String serverId, Set channelIds) { + DonkeyDao dao = getDelegateDao(); + + try { + return dao.getChannelStatistics(serverId, channelIds); + } finally { + dao.close(); + } + } + @Override public Statistics getChannelTotalStatistics(String serverId) { DonkeyDao dao = getDelegateDao(); @@ -589,6 +600,17 @@ public Statistics getChannelTotalStatistics(String serverId) { dao.close(); } } + + @Override + public Statistics getChannelTotalStatistics(String serverId, Set channelIds) { + DonkeyDao dao = getDelegateDao(); + + try { + return dao.getChannelTotalStatistics(serverId, channelIds); + } finally { + dao.close(); + } + } @Override public List getPortsInUse() { diff --git a/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDao.java b/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDao.java index a22f8fb1a1..8d7a303b58 100644 --- a/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDao.java +++ b/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDao.java @@ -208,6 +208,12 @@ public void insertMessageContent(MessageContent messageContent) { insertContent(messageContent.getChannelId(), messageContent.getMessageId(), messageContent.getMetaDataId(), messageContent.getContentType(), messageContent.getContent(), messageContent.getDataType(), messageContent.isEncrypted()); } + /* + * The statement is deliberately left open here. It is the cached statement that holds the + * accumulated batch, so closing it would discard every addBatch() made so far; + * executeBatchInsertMessageContent() runs the batch and closes the statement if the + * subclass needs that. + */ @Override public void batchInsertMessageContent(MessageContent messageContent) { logger.debug(messageContent.getChannelId() + "/" + messageContent.getMessageId() + "/" + messageContent.getMetaDataId() + ": batch inserting message content (" + messageContent.getContentType().toString() + ")"); @@ -237,9 +243,9 @@ public void batchInsertMessageContent(MessageContent messageContent) { statement.addBatch(); statement.clearParameters(); } catch (SQLException e) { + // The batch will never be executed now, so do not leave it for the next message. + clearBatchQuietly(statement); throw new DonkeyDaoException(e); - } finally { - closeDatabaseObjectIfNeeded(statement); } } @@ -258,14 +264,29 @@ public void executeBatchInsertMessageContent(String channelId) { */ statement = prepareStatement("batchInsertMessageContent", channelId); statement.executeBatch(); - statement.clearBatch(); } catch (SQLException e) { throw new DonkeyDaoException(e); } finally { + clearBatchQuietly(statement); closeDatabaseObjectIfNeeded(statement); } } + /** + * Empties a cached statement's batch without letting the cleanup itself fail. The statement + * outlives the DAO in the prepared statement cache, so anything left on its batch would be + * executed along with the next message's rows. + */ + private void clearBatchQuietly(Statement statement) { + if (statement != null) { + try { + statement.clearBatch(); + } catch (SQLException e) { + logger.debug("Failed to clear batch", e); + } + } + } + @Override public void storeMessageContent(MessageContent messageContent) { logger.debug(messageContent.getChannelId() + "/" + messageContent.getMessageId() + "/" + messageContent.getMetaDataId() + ": updating message content (" + messageContent.getContentType().toString() + ")"); @@ -2332,21 +2353,35 @@ public void resetAllStatistics(String channelId) { @Override public Statistics getChannelStatistics(String serverId) { - return getChannelStatistics(serverId, false); + return getChannelStatistics(serverId, null, false); + } + + @Override + public Statistics getChannelStatistics(String serverId, Set channelIds) { + return getChannelStatistics(serverId, channelIds, false); } @Override public Statistics getChannelTotalStatistics(String serverId) { - return getChannelStatistics(serverId, true); + return getChannelStatistics(serverId, null, true); } - private Statistics getChannelStatistics(String serverId, boolean total) { - Map channelIds = getLocalChannelIds(); + @Override + public Statistics getChannelTotalStatistics(String serverId, Set channelIds) { + return getChannelStatistics(serverId, channelIds, true); + } + + private Statistics getChannelStatistics(String serverId, Set requestedChannelIds, boolean total) { + Map localChannelIds = getLocalChannelIds(); String queryId = (total) ? "getChannelTotalStatistics" : "getChannelStatistics"; Statistics statistics = new Statistics(!total); ResultSet resultSet = null; - for (String channelId : channelIds.keySet()) { + for (String channelId : localChannelIds.keySet()) { + if (CollectionUtils.isNotEmpty(requestedChannelIds) && !requestedChannelIds.contains(channelId)) { + continue; + } + PreparedStatement statement = null; try { statement = prepareStatement(queryId, channelId); @@ -2865,6 +2900,7 @@ private Map getMetaDataMap(String channelId, long messageId, int // do not cache this statement since metadata columns may be added/removed statement = connection.prepareStatement(querySource.getQuery("getMetaDataMap", values)); + disableServerSidePlanCache(statement); statement.setLong(1, messageId); statement.setInt(2, metaDataId); @@ -2933,6 +2969,7 @@ private Map>> getMetaDataMaps(String chan // do not cache this statement since metadata columns may be added/removed statement = connection.prepareStatement(querySource.getQuery("getMetaDataMapByMessageId", values)); + disableServerSidePlanCache(statement); resultSet = statement.executeQuery(); ResultSetMetaData resultSetMetaData = resultSet.getMetaData(); @@ -3072,6 +3109,14 @@ PreparedStatement prepareStatement(String queryId, String channelId) throws SQLE return statementSource.getPreparedStatement(queryId, localChannelId); } + /** + * Stops the driver from caching a server-side plan for one statement where the schema may have + * changed from query to query. + */ + protected void disableServerSidePlanCache(PreparedStatement statement) throws SQLException { + // nop for most drivers + } + protected void close(Statement statement) { try { DbUtils.close(statement); diff --git a/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/MysqlDaoFactory.java b/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/MysqlDaoFactory.java index ab08ebf458..c16c27767b 100644 --- a/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/MysqlDaoFactory.java +++ b/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/MysqlDaoFactory.java @@ -9,13 +9,35 @@ package com.mirth.connect.donkey.server.data.jdbc; +import java.sql.Connection; +import java.sql.SQLException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + import com.mirth.connect.donkey.util.SerializerProvider; public class MysqlDaoFactory extends JdbcDaoFactory { + private Logger logger = LogManager.getLogger(this.getClass()); + @Override public JdbcDao getDao(SerializerProvider serializerProvider) { JdbcDao dao = super.getDao(serializerProvider); dao.setQuoteChar('`'); + checkTransactionIsolation(dao); return dao; } + + /** + * Warns once if the connection is using the default REPEATABLE-READ transaction isolation level. + */ + private void checkTransactionIsolation(JdbcDao dao) { + try { + if (dao.getConnection().getTransactionIsolation() == Connection.TRANSACTION_REPEATABLE_READ) { + logger.error("This MySQL connection is not using the READ-COMMITTED transaction isolation level. The MySQL REPEATABLE-READ will cause intermittent issues. Set the transaction isolation level in the database URL or the database server to READ-COMMITTED to avoid these problems."); + } + } catch (SQLException e) { + logger.error("Failed to check the transaction isolation level for this MySQL connection.", e); + } + } } diff --git a/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/PostgresqlDaoFactory.java b/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/PostgresqlDaoFactory.java index da72f47388..7e79b17982 100644 --- a/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/PostgresqlDaoFactory.java +++ b/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/PostgresqlDaoFactory.java @@ -9,13 +9,17 @@ package com.mirth.connect.donkey.server.data.jdbc; +import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.SQLException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import com.mirth.connect.donkey.server.Donkey; +import com.mirth.connect.donkey.server.channel.Statistics; import com.mirth.connect.donkey.server.data.DonkeyDaoException; +import com.mirth.connect.donkey.server.data.StatisticsUpdater; import com.mirth.connect.donkey.util.SerializerProvider; public class PostgresqlDaoFactory extends JdbcDaoFactory { @@ -33,6 +37,11 @@ public JdbcDao getDao(SerializerProvider serializerProvider) { return dao; } + @Override + protected JdbcDao getDao(Donkey donkey, Connection connection, QuerySource querySource, PreparedStatementSource statementSource, SerializerProvider serializerProvider, boolean encryptMessageContent, boolean encryptAttachments, boolean encryptCustomMetaData, boolean decryptData, StatisticsUpdater statisticsUpdater, Statistics currentStats, Statistics totalStats, String statsServerId) { + return new PostgresqlJdbcDao(donkey, connection, querySource, statementSource, serializerProvider, encryptMessageContent, encryptAttachments, encryptCustomMetaData, decryptData, statisticsUpdater, currentStats, totalStats, statsServerId); + } + private String getAsyncCommitCommand(JdbcDao dao) { if (supported == null) { try { diff --git a/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/PostgresqlJdbcDao.java b/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/PostgresqlJdbcDao.java new file mode 100644 index 0000000000..07c272542e --- /dev/null +++ b/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/PostgresqlJdbcDao.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) Mirth Corporation. All rights reserved. + * + * http://www.mirthcorp.com + * + * The software in this package is published under the terms of the MPL license a copy of which has + * been included with this distribution in the LICENSE.txt file. + */ + +package com.mirth.connect.donkey.server.data.jdbc; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.postgresql.PGStatement; + +import com.mirth.connect.donkey.server.Donkey; +import com.mirth.connect.donkey.server.channel.Statistics; +import com.mirth.connect.donkey.server.data.StatisticsUpdater; +import com.mirth.connect.donkey.util.SerializerProvider; + +public class PostgresqlJdbcDao extends JdbcDao { + + public PostgresqlJdbcDao(Donkey donkey, Connection connection, QuerySource querySource, PreparedStatementSource statementSource, SerializerProvider serializerProvider, boolean encryptMessageContent, boolean encryptAttachments, boolean encryptCustomMetaData, boolean decryptData, StatisticsUpdater statisticsUpdater, Statistics currentStats, Statistics totalStats, String statsServerId) { + super(donkey, connection, querySource, statementSource, serializerProvider, encryptMessageContent, encryptAttachments, encryptCustomMetaData, decryptData, statisticsUpdater, currentStats, totalStats, statsServerId); + } + + /** Excludes a statement from caching. Required when schema changes at runtime. */ + @Override + protected void disableServerSidePlanCache(PreparedStatement statement) throws SQLException { + statement.unwrap(PGStatement.class).setPrepareThreshold(0); + } + +} diff --git a/donkey/src/main/java/com/mirth/connect/donkey/server/data/passthru/PassthruDao.java b/donkey/src/main/java/com/mirth/connect/donkey/server/data/passthru/PassthruDao.java index 9856290c6f..1d24dd06f0 100644 --- a/donkey/src/main/java/com/mirth/connect/donkey/server/data/passthru/PassthruDao.java +++ b/donkey/src/main/java/com/mirth/connect/donkey/server/data/passthru/PassthruDao.java @@ -340,11 +340,21 @@ public Statistics getChannelStatistics(String serverId) { return new Statistics(false); } + @Override + public Statistics getChannelStatistics(String serverId, Set channelIds) { + return new Statistics(false); + } + @Override public Statistics getChannelTotalStatistics(String serverId) { return new Statistics(false); } + @Override + public Statistics getChannelTotalStatistics(String serverId, Set channelIds) { + return new Statistics(false); + } + @Override public List getConnectorMessages(String channelId, long messageId, Set metaDataIds, boolean includeContent) { return null; diff --git a/donkey/src/main/java/com/mirth/connect/donkey/server/data/timed/TimedDao.java b/donkey/src/main/java/com/mirth/connect/donkey/server/data/timed/TimedDao.java index b6234ef99d..a16a760213 100644 --- a/donkey/src/main/java/com/mirth/connect/donkey/server/data/timed/TimedDao.java +++ b/donkey/src/main/java/com/mirth/connect/donkey/server/data/timed/TimedDao.java @@ -662,6 +662,17 @@ public Statistics getChannelStatistics(String serverId) { } } + @Override + public Statistics getChannelStatistics(String serverId, Set channelIds) { + long startTime = System.currentTimeMillis(); + + try { + return dao.getChannelStatistics(serverId, channelIds); + } finally { + timer.log("getChannelStatistics", System.currentTimeMillis() - startTime); + } + } + @Override public Statistics getChannelTotalStatistics(String serverId) { long startTime = System.currentTimeMillis(); @@ -672,6 +683,17 @@ public Statistics getChannelTotalStatistics(String serverId) { timer.log("getChannelTotalStatistics", System.currentTimeMillis() - startTime); } } + + @Override + public Statistics getChannelTotalStatistics(String serverId, Set channelIds) { + long startTime = System.currentTimeMillis(); + + try { + return dao.getChannelTotalStatistics(serverId, channelIds); + } finally { + timer.log("getChannelTotalStatistics", System.currentTimeMillis() - startTime); + } + } @Override public List getPortsInUse() { diff --git a/donkey/src/test/java/com/mirth/connect/donkey/server/data/jdbc/OracleJdbcDaoTest.java b/donkey/src/test/java/com/mirth/connect/donkey/server/data/jdbc/OracleJdbcDaoTest.java new file mode 100644 index 0000000000..fd9cc28934 --- /dev/null +++ b/donkey/src/test/java/com/mirth/connect/donkey/server/data/jdbc/OracleJdbcDaoTest.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) Mirth Corporation. All rights reserved. + * + * http://www.mirthcorp.com + * + * The software in this package is published under the terms of the MPL license a copy of which has + * been included with this distribution in the LICENSE.txt file. + */ + +package com.mirth.connect.donkey.server.data.jdbc; + +import static org.mockito.ArgumentMatchers.eq; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; + +import com.mirth.connect.donkey.model.message.ContentType; +import com.mirth.connect.donkey.model.message.MessageContent; +import com.mirth.connect.donkey.server.Donkey; +import com.mirth.connect.donkey.server.channel.Statistics; +import com.mirth.connect.donkey.server.data.DonkeyDaoException; +import com.mirth.connect.donkey.server.data.StatisticsUpdater; +import com.mirth.connect.donkey.util.SerializerProvider; + +/** + * Oracle is the only DAO that actually closes statements, so it is the only one where closing + * the wrong statement at the wrong time is observable. + */ +public class OracleJdbcDaoTest { + + private static final String CHANNEL_ID = "abc"; + + private OracleJdbcDao dao; + private PreparedStatement statement; + + @Before + public void before() throws SQLException { + Donkey donkey = mock(Donkey.class); + Connection connection = mock(Connection.class); + QuerySource querySource = mock(QuerySource.class); + PreparedStatementSource statementSource = mock(PreparedStatementSource.class); + SerializerProvider serializerProvider = mock(SerializerProvider.class); + StatisticsUpdater statisticsUpdater = mock(StatisticsUpdater.class); + Statistics currentStats = mock(Statistics.class); + Statistics totalStats = mock(Statistics.class); + + dao = spy(new OracleJdbcDao(donkey, connection, querySource, statementSource, serializerProvider, false, false, false, false, statisticsUpdater, currentStats, totalStats, "")); + + statement = mock(PreparedStatement.class); + doReturn(statement).when(dao).prepareStatement(eq("batchInsertMessageContent"), eq(CHANNEL_ID)); + } + + /** + * The batch lives on the cached statement, so the statement has to survive every + * batchInsertMessageContent() call and only be closed once the batch has been executed. + * Closing it earlier silently discarded the source content on Oracle. + */ + @Test + public void testBatchInsertMessageContentKeepsStatementOpenUntilExecuted() throws SQLException { + dao.batchInsertMessageContent(content(ContentType.PROCESSED_RAW, "processed raw")); + dao.batchInsertMessageContent(content(ContentType.TRANSFORMED, "transformed")); + dao.batchInsertMessageContent(content(ContentType.ENCODED, "encoded")); + + verify(statement, times(3)).addBatch(); + verify(statement, never()).close(); + + dao.executeBatchInsertMessageContent(CHANNEL_ID); + + InOrder inOrder = inOrder(statement); + inOrder.verify(statement, times(3)).addBatch(); + inOrder.verify(statement).executeBatch(); + inOrder.verify(statement).clearBatch(); + inOrder.verify(statement).close(); + } + + /** A failed batch must not be left behind for the next message to execute. */ + @Test + public void testFailedBatchInsertClearsTheBatch() throws SQLException { + doThrow(new SQLException("no")).when(statement).addBatch(); + + try { + dao.batchInsertMessageContent(content(ContentType.ENCODED, "encoded")); + fail("Expected a DonkeyDaoException"); + } catch (DonkeyDaoException e) { + // expected + } + + verify(statement).clearBatch(); + } + + private static MessageContent content(ContentType contentType, String content) { + return new MessageContent(CHANNEL_ID, 1L, 0, contentType, content, "RAW", false); + } +} diff --git a/donkey/src/test/java/com/mirth/connect/donkey/server/queue/ConnectorMessageQueueTest.java b/donkey/src/test/java/com/mirth/connect/donkey/server/queue/ConnectorMessageQueueTest.java new file mode 100644 index 0000000000..4cc8b63c14 --- /dev/null +++ b/donkey/src/test/java/com/mirth/connect/donkey/server/queue/ConnectorMessageQueueTest.java @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Mitch Gaffigan + +package com.mirth.connect.donkey.server.queue; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.mirth.connect.donkey.model.message.ConnectorMessage; +import com.mirth.connect.donkey.model.message.Status; +import com.mirth.connect.donkey.server.Donkey; +import com.mirth.connect.donkey.server.event.EventDispatcher; + +/** + * The buffer arithmetic in {@link ConnectorMessageQueue}: how much of the queue is held in + * memory, what {@code add} does once the buffer is full, and that polling still walks the + * whole queue in order by going back to the data source. + * + *

The queue is a window onto rows the engine has already committed, so the data source + * here is a plain map standing in for those rows rather than a database. That is the whole + * point of testing it at this level: the arithmetic is the same on every dialect, and a fake + * makes the buffer boundaries exact instead of a race against a live channel. + */ +public class ConnectorMessageQueueTest { + + private static final String CHANNEL_ID = "ConnectorMessageQueueTest"; + private static final String CHANNEL_NAME = "ConnectorMessageQueueTest"; + private static final String SERVER_ID = "ConnectorMessageQueueTestServer"; + private static final int META_DATA_ID = 0; + + private static final int BUFFER_CAPACITY = 3; + private static final int QUEUE_SIZE = 10; + + private FakeDataSource dataSource; + private SourceQueue queue; + + /** + * {@link ConnectorMessageQueue} dispatches a queue-size event on every mutation and reads + * its dispatcher off the {@link Donkey} singleton, which is only populated by a started + * engine. Inject a mock one instead, as ChannelTest does. + */ + @BeforeClass + public static void injectDonkey() { + Donkey donkey = mock(Donkey.class); + when(donkey.getEventDispatcher()).thenReturn(mock(EventDispatcher.class)); + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + requestStaticInjection(Donkey.class); + bind(Donkey.class).toInstance(donkey); + } + }); + injector.getInstance(Donkey.class); + + assertTrue("Donkey singleton injection failed", donkey == Donkey.getInstance()); + } + + @Before + public void setUp() { + dataSource = new FakeDataSource(); + queue = new SourceQueue(); + queue.setBufferCapacity(BUFFER_CAPACITY); + queue.setDataSource(dataSource); + } + + /** + * The buffer is a window, not a copy: filling it from a data source holding more messages + * than the buffer can take leaves the queue reporting the full size and the buffer the + * capacity. + */ + @Test + public void fillBufferReadsAtMostTheBufferCapacity() { + storeMessages(QUEUE_SIZE); + + queue.updateSize(); + queue.fillBuffer(); + + assertEquals(BUFFER_CAPACITY, queue.getBufferCapacity()); + assertEquals(QUEUE_SIZE, queue.size()); + assertEquals(BUFFER_CAPACITY, queue.getBufferSize()); + } + + /** + * {@code add} is called once per message the engine has just committed. Up to the capacity + * it puts the message straight into the buffer; past it the buffer stops growing while the + * queue keeps counting, so the extra messages are only reachable through the data source. + */ + @Test + public void addPastTheBufferCapacityGrowsTheQueueButNotTheBuffer() { + syncWithEmptyDataSource(); + + for (int i = 1; i <= BUFFER_CAPACITY; i++) { + queue.add(storeMessage(i)); + assertEquals("queue size after adding message " + i, i, queue.size()); + assertEquals("buffer size after adding message " + i, i, queue.getBufferSize()); + } + + for (int i = BUFFER_CAPACITY + 1; i <= QUEUE_SIZE; i++) { + queue.add(storeMessage(i)); + assertEquals("queue size after adding message " + i, i, queue.size()); + assertEquals("buffer size after adding message " + i, BUFFER_CAPACITY, queue.getBufferSize()); + } + } + + /** + * Polling past the buffer is the behaviour the capacity exists for: the queue refills from + * the data source when the buffer runs dry, so every message comes out exactly once and in + * the order the data source holds them, and the queue ends empty. + * + *

Every message here is committed and then added, which is what the engine guarantees by + * doing both under this queue's own lock. + * {@link #rowsCommittedWithoutBeingAddedAreStrandedUntilTheQueueResyncs} is the counterfactual + * for what that lock is holding off. + */ + @Test + public void pollingRefillsTheBufferAndReturnsEveryMessageInOrder() { + syncWithEmptyDataSource(); + for (int i = 1; i <= QUEUE_SIZE; i++) { + queue.add(storeMessage(i)); + } + + List polled = drain(); + + assertEquals(expectedIds(QUEUE_SIZE), polled); + assertEquals(0, queue.size()); + assertTrue("the buffer only holds " + BUFFER_CAPACITY + " of " + QUEUE_SIZE + " messages, so" + + " draining the queue must have gone back to the data source for more", + dataSource.readCount > 1); + } + + /** + * The queue's size is a count of what has been added to it, not of what is committed, so a + * row committed without a matching {@code add} is invisible to it: the queue hands over the + * message it does know about, then reports itself empty while those rows are still there. + * + *

This is why the engine commits a source message and adds it to the queue under the same + * lock. Nothing a client can do opens this window - it takes a write that bypasses the queue, + * which is what this test does directly - but it is also what an unclean shutdown leaves + * behind, and the recovery at the end is the same resync a redeploy performs. + */ + @Test + public void rowsCommittedWithoutBeingAddedAreStrandedUntilTheQueueResyncs() { + syncWithEmptyDataSource(); + + // Committed, but the add never happened. + int strandedCount = BUFFER_CAPACITY + 1; + storeMessages(strandedCount); + + // A later message committed and added the ordinary way. + long addedId = strandedCount + 1; + queue.add(storeMessage(addedId)); + + assertEquals("the queue counts what was added to it", 1, queue.size()); + assertEquals("every message is committed", strandedCount + 1, dataSource.getSize()); + + // The one message the queue knows about comes out, and then it calls itself empty. + ConnectorMessage polled = queue.poll(); + assertEquals(addedId, polled.getMessageId()); + dataSource.rows.remove(polled.getMessageId()); + queue.finish(polled); + assertNull("the queue reports itself empty while committed rows are still there", + queue.poll()); + + // Resyncing against the data source is what finds them again, in order. + queue.invalidate(false, true); + queue.updateSize(); + + assertEquals(strandedCount, queue.size()); + assertEquals(expectedIds(strandedCount), drain()); + } + + /** + * Shrinking the capacity discards the buffer rather than truncating it, because the + * messages it holds may no longer be the ones a smaller window should start from. The + * queue size is untouched and the next poll refills, so nothing is lost or reordered. + */ + @Test + public void shrinkingTheBufferCapacityDiscardsTheBufferWithoutLosingMessages() { + storeMessages(QUEUE_SIZE); + queue.updateSize(); + queue.fillBuffer(); + assertEquals(BUFFER_CAPACITY, queue.getBufferSize()); + + queue.setBufferCapacity(1); + + assertEquals(0, queue.getBufferSize()); + assertEquals(QUEUE_SIZE, queue.size()); + assertEquals(expectedIds(QUEUE_SIZE), drain()); + } + + /** + * Brings the queue in sync with an empty data source. {@code setDataSource} invalidates the + * queue, and until it is filled once, {@code add} takes its resync path rather than the + * buffering path under test. + */ + private void syncWithEmptyDataSource() { + queue.updateSize(); + queue.fillBuffer(); + assertEquals(0, queue.size()); + } + + /** + * Polls the queue dry the way a queue thread does: every message that comes out is finished + * and its row removed, so a refill sees only what is genuinely still queued. + */ + private List drain() { + List polled = new ArrayList(); + + ConnectorMessage connectorMessage; + while ((connectorMessage = queue.poll()) != null) { + polled.add(connectorMessage.getMessageId()); + dataSource.rows.remove(connectorMessage.getMessageId()); + queue.finish(connectorMessage); + } + + assertNull(queue.poll()); + return polled; + } + + private void storeMessages(int count) { + for (int i = 1; i <= count; i++) { + storeMessage(i); + } + } + + private ConnectorMessage storeMessage(long messageId) { + ConnectorMessage connectorMessage = new ConnectorMessage(CHANNEL_ID, CHANNEL_NAME, messageId, META_DATA_ID, + SERVER_ID, Calendar.getInstance(), Status.RECEIVED); + dataSource.rows.put(messageId, connectorMessage); + return connectorMessage; + } + + private static List expectedIds(int count) { + List ids = new ArrayList(); + for (long i = 1; i <= count; i++) { + ids.add(i); + } + return ids; + } + + /** + * The committed queue rows, in message id order. The real data source runs a bounded + * {@code getConnectorMessages} query against the channel's queue table; this one pages the + * same way over a map, and counts the reads so a test can tell a refill happened. + */ + private static final class FakeDataSource extends ConnectorMessageQueueDataSource { + + final Map rows = new LinkedHashMap(); + int readCount; + + FakeDataSource() { + super(CHANNEL_ID, SERVER_ID, META_DATA_ID, Status.RECEIVED, false, null); + } + + @Override + public int getSize() { + return rows.size(); + } + + @Override + public Map getItems(int offset, int limit) { + readCount++; + + Map page = new LinkedHashMap(); + for (ConnectorMessage connectorMessage : rows.values()) { + if (offset > 0) { + offset--; + } else if (page.size() < limit) { + page.put(connectorMessage.getMessageId(), connectorMessage); + } else { + break; + } + } + return page; + } + } +} diff --git a/donkey/src/test/java/com/mirth/connect/donkey/test/ChannelControllerTests.java b/donkey/src/test/java/com/mirth/connect/donkey/test/ChannelControllerTests.java index 4927f9b60c..b7df00e374 100644 --- a/donkey/src/test/java/com/mirth/connect/donkey/test/ChannelControllerTests.java +++ b/donkey/src/test/java/com/mirth/connect/donkey/test/ChannelControllerTests.java @@ -11,14 +11,6 @@ import static org.junit.Assert.assertEquals; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.Types; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.AfterClass; @@ -26,21 +18,21 @@ import org.junit.BeforeClass; import org.junit.Test; -import com.mirth.connect.donkey.model.message.Status; import com.mirth.connect.donkey.server.Donkey; import com.mirth.connect.donkey.server.StartException; -import com.mirth.connect.donkey.server.channel.Channel; import com.mirth.connect.donkey.server.controllers.ChannelController; import com.mirth.connect.donkey.server.data.timed.TimedDaoFactory; -import com.mirth.connect.donkey.test.util.TestSourceConnector; import com.mirth.connect.donkey.test.util.TestUtils; import com.mirth.connect.donkey.util.ActionTimer; +/** + * What is left of these tests after the statistics ones moved to the CI harness: the statistics + * cases live in {@code smoketest/.../*StatisticsTest.java} (250-statistics), which reads the same + * counters over the client API against every dialect instead of against the database directly. + */ public class ChannelControllerTests { private static int TEST_SIZE = 10; private static String channelId = TestUtils.DEFAULT_CHANNEL_ID; - private static String serverId = TestUtils.DEFAULT_SERVER_ID; - private static String testMessage = TestUtils.TEST_HL7_MESSAGE; private static ActionTimer daoTimer = new ActionTimer(); private Logger logger = LogManager.getLogger(this.getClass()); @@ -89,145 +81,4 @@ final public void testGetLocalChannelId() throws Exception { ChannelController.getInstance().removeChannel(channelId); } } - - /* - * Remove the channel corresponding to channelId (if applicable), assert: - The channel - * statistics in the database are equal to the ones returned from getStatistics - * - * Create a new channel (use channelId), then assert: - The channel statistics in the database - * are equal to the ones returned from getStatistics - * - * Send messages through the channel, and after each message, assert: - The channel statistics - * in the database are equal to the ones returned from getStatistics - */ - @Test - public final void testGetTotals() throws Exception { - Channel channel = null; - - try { - logger.info("Testing ChannelController.getTotals..."); - - ChannelController.getInstance().removeChannel(channelId); - assertEquals(TestUtils.getChannelStatistics(channelId), ChannelController.getInstance().getStatistics().getChannelStats(channelId)); - - channel = TestUtils.createDefaultChannel(channelId, serverId); - channel.deploy(); - channel.start(null); - - assertEquals(TestUtils.getChannelStatistics(channel.getChannelId()), ChannelController.getInstance().getStatistics().getChannelStats(channelId)); - - for (int i = 1; i <= TEST_SIZE; i++) { - ((TestSourceConnector) channel.getSourceConnector()).readTestMessage(testMessage); - - assertEquals(TestUtils.getChannelStatistics(channel.getChannelId()), ChannelController.getInstance().getStatistics().getChannelStats(channelId)); - } - - System.out.println(daoTimer.getLog()); - } finally { - if (channel != null) { - channel.stop(); - channel.undeploy(); - ChannelController.getInstance().removeChannel(channel.getChannelId()); - } - } - } - - /* - * Create a new default channel Insert some random initial source/destination/aggregate - * statistics Start up the channel, send messages, and after each message assert: - The - * statistics returned from getStatistics is equal to the difference between the statistics in - * the database and the initial statistics - */ - @Test - public final void testGetStatistics() throws Exception { - Channel channel = TestUtils.createDefaultChannel(channelId, serverId); - - try { - logger.info("Testing ChannelController.getStatistics..."); - - // Insert some initial statistics - Connection connection = null; - PreparedStatement statement = null; - try { - long localChannelId = ChannelController.getInstance().getLocalChannelId(channelId); - connection = TestUtils.getConnection(); - statement = connection.prepareStatement("DELETE FROM d_ms" + localChannelId); - statement.executeUpdate(); - statement.close(); - - for (Integer metaDataId : new Integer[] { null, 0, 1 }) { - statement = connection.prepareStatement("INSERT INTO d_ms" + localChannelId + " (metadata_id, received, filtered, transformed, pending, sent, error) VALUES (?,?,?,?,?,?,?)"); - if (metaDataId != null) { - statement.setInt(1, metaDataId); - } else { - statement.setNull(1, Types.INTEGER); - } - for (int i = 2; i <= 7; i++) { - statement.setInt(i, (int) (Math.random() * 100)); - } - statement.executeUpdate(); - statement.close(); - } - connection.commit(); - } finally { - TestUtils.close(statement); - TestUtils.close(connection); - } - - // Get the initial statistics - Map> initialStats = TestUtils.getChannelStatistics(channel.getChannelId()); - - channel.deploy(); - channel.start(null); - - // Send messages - for (int i = 1; i <= TEST_SIZE; i++) { - ((TestSourceConnector) channel.getSourceConnector()).readTestMessage(testMessage); - - Map> dbStats = TestUtils.getChannelStatistics(channel.getChannelId()); - Map> vmStats = ChannelController.getInstance().getStatistics().getChannelStats(channel.getChannelId()); - Map> subtractedStats = subtractStats(dbStats, initialStats); - - // Assert that getStatistics returns the difference between the current database statistics and the initial statistics - assertEquals(subtractedStats, vmStats); - } - - System.out.println(daoTimer.getLog()); - } finally { - channel.stop(); - channel.undeploy(); - ChannelController.getInstance().removeChannel(channel.getChannelId()); - } - } - - private Map> subtractStats(Map> minuend, Map> subtrahend) { - Map> stats = new HashMap>(); - - for (Integer metaDataId : joinSets(minuend.keySet(), subtrahend.keySet())) { - Map connectorStats = new HashMap(); - - for (Status status : Status.values()) { - if (status != Status.QUEUED) { - connectorStats.put(status, 0L); - if (minuend.containsKey(metaDataId) && minuend.get(metaDataId).containsKey(status)) { - connectorStats.put(status, minuend.get(metaDataId).get(status)); - } - if (subtrahend.containsKey(metaDataId) && subtrahend.get(metaDataId).containsKey(status)) { - connectorStats.put(status, connectorStats.get(status) - subtrahend.get(metaDataId).get(status)); - } - } - } - - stats.put(metaDataId, connectorStats); - } - - return stats; - } - - private Set joinSets(Set set1, Set set2) { - Set joinedSet = new HashSet(); - joinedSet.addAll(set1); - joinedSet.addAll(set2); - return joinedSet; - } } diff --git a/donkey/src/test/java/com/mirth/connect/donkey/test/ChannelTests.java b/donkey/src/test/java/com/mirth/connect/donkey/test/ChannelTests.java index 8a532f21ba..146e62e69d 100644 --- a/donkey/src/test/java/com/mirth/connect/donkey/test/ChannelTests.java +++ b/donkey/src/test/java/com/mirth/connect/donkey/test/ChannelTests.java @@ -15,44 +15,23 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import java.math.BigDecimal; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.List; - import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import com.mirth.connect.donkey.model.channel.DeployedState; -import com.mirth.connect.donkey.model.channel.DestinationConnectorProperties; -import com.mirth.connect.donkey.model.channel.DestinationConnectorPropertiesInterface; -import com.mirth.connect.donkey.model.channel.MetaDataColumn; -import com.mirth.connect.donkey.model.channel.MetaDataColumnException; -import com.mirth.connect.donkey.model.channel.MetaDataColumnType; -import com.mirth.connect.donkey.model.message.ConnectorMessage; -import com.mirth.connect.donkey.model.message.ContentType; import com.mirth.connect.donkey.model.message.Message; -import com.mirth.connect.donkey.model.message.MessageContent; -import com.mirth.connect.donkey.model.message.RawMessage; import com.mirth.connect.donkey.server.Donkey; import com.mirth.connect.donkey.server.StartException; import com.mirth.connect.donkey.server.channel.Channel; import com.mirth.connect.donkey.server.channel.ChannelException; import com.mirth.connect.donkey.server.channel.DestinationChainProvider; import com.mirth.connect.donkey.server.channel.DestinationConnector; -import com.mirth.connect.donkey.server.channel.DispatchResult; -import com.mirth.connect.donkey.server.channel.SourceConnector; -import com.mirth.connect.donkey.server.channel.StorageSettings; import com.mirth.connect.donkey.server.controllers.ChannelController; import com.mirth.connect.donkey.test.util.TestChannel; import com.mirth.connect.donkey.test.util.TestDestinationConnector; -import com.mirth.connect.donkey.test.util.TestFilterTransformer; -import com.mirth.connect.donkey.test.util.TestPostProcessor; -import com.mirth.connect.donkey.test.util.TestPreProcessor; import com.mirth.connect.donkey.test.util.TestSourceConnector; import com.mirth.connect.donkey.test.util.TestUtils; -import com.mirth.connect.donkey.test.util.TestUtils.MessageStorageMode; public class ChannelTests { final public static int TEST_SIZE = 50; @@ -321,400 +300,20 @@ public final void testControllerRemoveChannel() throws Exception { } /* - * Creates and deploys a channel, and asserts that: - No extra columns exist on the custom - * metadata table - * - * Adds metadata columns (one of each type), redeploys the channel, and asserts that: - All the - * added columns are in the database with the correct name and type - * - * Removes one of the columns, redeploys, and asserts that: - The column is no longer in the - * database - * - * Alters the name of one of the columns, redeploys, and asserts that: - The old column is - * dropped correctly - The new column is added correctly - * - * Alters the type one of the columns, redeploys, and asserts that: - The old column is dropped - * correctly - The new column is added correctly - * - * Alters the name and type of one of the columns, redeploys, and asserts that: - The old column - * is dropped correctly - The new column is added correctly + * Replaced by EncryptionAtRestTest: a channel with encryptData set stores every content stage + * as ciphertext, and the decrypting read path hands the original back. */ - @Test - public final void testUpdateMetaDataColumns() throws Exception { - ChannelController.getInstance().removeChannel(channelId); - - TestChannel channel = (TestChannel) TestUtils.createDefaultChannel(channelId, serverId); - - channel.deploy(); - - // Assert that there are no columns currently - assertEquals(TestUtils.getExistingMetaDataColumns(channelId).size(), 0); - - // Add all the columns - channel.getMetaDataColumns().add(new MetaDataColumn("stringcolumn", MetaDataColumnType.STRING, null)); - channel.getMetaDataColumns().add(new MetaDataColumn("numbercolumn", MetaDataColumnType.NUMBER, null)); - channel.getMetaDataColumns().add(new MetaDataColumn("booleancolumn", MetaDataColumnType.BOOLEAN, null)); - channel.getMetaDataColumns().add(new MetaDataColumn("timestampcolumn", MetaDataColumnType.TIMESTAMP, null)); - - channel.undeploy(); - channel.deploy(); - - // Assert that each column exists - List columns = TestUtils.getExistingMetaDataColumns(channelId); - assertTrue(columns.contains(new MetaDataColumn("stringcolumn", MetaDataColumnType.STRING, null))); - assertTrue(columns.contains(new MetaDataColumn("numbercolumn", MetaDataColumnType.NUMBER, null))); - assertTrue(columns.contains(new MetaDataColumn("booleancolumn", MetaDataColumnType.BOOLEAN, null))); - assertTrue(columns.contains(new MetaDataColumn("timestampcolumn", MetaDataColumnType.TIMESTAMP, null))); - - // Remove the string column - channel.getMetaDataColumns().remove(0); - - channel.undeploy(); - channel.deploy(); - - // Assert that the string column doesn't exist anymore - columns = TestUtils.getExistingMetaDataColumns(channelId); - assertFalse(columns.contains(new MetaDataColumn("stringcolumn", MetaDataColumnType.STRING, null))); - - // Alter the long column's name - channel.getMetaDataColumns().get(0).setName("longcolumn2"); - - channel.undeploy(); - channel.deploy(); - - // Assert that the long column got dropped/added correctly - columns = TestUtils.getExistingMetaDataColumns(channelId); - assertFalse(columns.contains(new MetaDataColumn("numbercolumn", MetaDataColumnType.NUMBER, null))); - assertTrue(columns.contains(new MetaDataColumn("numbercolumn2", MetaDataColumnType.NUMBER, null))); - - // Alter the double column's type - channel.getMetaDataColumns().get(1).setType(MetaDataColumnType.TIMESTAMP); - - channel.undeploy(); - channel.deploy(); - - // Assert that the double column got dropped/added correctly as a timestamp column - columns = TestUtils.getExistingMetaDataColumns(channelId); - assertFalse(columns.contains(new MetaDataColumn("numbercolumn", MetaDataColumnType.NUMBER, null))); - assertTrue(columns.contains(new MetaDataColumn("numbercolumn", MetaDataColumnType.TIMESTAMP, null))); - - // Alter the boolean column's name and type - channel.getMetaDataColumns().get(2).setName("booleancolumn2"); - channel.getMetaDataColumns().get(2).setType(MetaDataColumnType.TIMESTAMP); - - channel.undeploy(); - channel.deploy(); - - // Assert that the boolean column got dropped/added correctly as a time column - columns = TestUtils.getExistingMetaDataColumns(channelId); - assertFalse(columns.contains(new MetaDataColumn("booleancolumn", MetaDataColumnType.BOOLEAN, null))); - assertTrue(columns.contains(new MetaDataColumn("booleancolumn2", MetaDataColumnType.TIMESTAMP, null))); - - channel.undeploy(); - } - - @Test - public final void testMetaDataCasting() throws MetaDataColumnException { - MetaDataColumnType columnType = MetaDataColumnType.BOOLEAN; - Boolean booleanValue = (Boolean) columnType.castValue("TRUE"); - assertEquals(Boolean.TRUE, booleanValue); - booleanValue = (Boolean) columnType.castValue("FALSE"); - assertEquals(Boolean.FALSE, booleanValue); - - columnType = MetaDataColumnType.NUMBER; - BigDecimal bigDecimalValue = (BigDecimal) columnType.castValue("1.0234567890123456789"); - assertEquals(new BigDecimal(1.0234567890123456789), bigDecimalValue); - - columnType = MetaDataColumnType.STRING; - String stringValue = (String) columnType.castValue(" test !@# String 123 "); - assertEquals(" test !@# String 123 ", stringValue); - - columnType = MetaDataColumnType.TIMESTAMP; - Calendar dateValue = (Calendar) columnType.castValue("2010-01-02 13:01:02"); - assertEquals("13 01 02 01 02 2010", new SimpleDateFormat("HH mm ss MM dd yyyy").format(dateValue.getTimeInMillis())); - } /* - * Create a new test channel Process a source message with a metadata ID of 1, assert that: - An - * InvalidConnectorMessageState exception is thrown - * - * Process a source message with a status other than RECEIVED, assert that: - An - * InvalidConnectorMessageState exception is thrown - * - * Process a valid source message, and assert that: - The pre-processor was run - The processed - * raw content was stored - The filter/transformer was run - The transformed/encoded content was - * stored - Initial messages were created for each destination chain - The message processed - * through at least the first destination connector for each chain - The post-processor was run - * - The final transaction was created + * Replaced by the ci/tests/200-custom-metadata-columns fixtures (a column of each type, + * value casting, and values that are absent or uncastable) and + * CustomMetaDataColumnRedeployTest (adding, removing and retyping a deployed channel's + * columns, which is what testUpdateMetaDataColumns redeployed for). */ - @Test - public final void testProcess() throws Exception { - TestChannel channel = (TestChannel) TestUtils.createDefaultChannel(channelId, serverId); - - channel.deploy(); - channel.start(null); - - ConnectorMessage sourceMessage = TestUtils.createAndStoreNewMessage(new RawMessage(testMessage), channelId, channelName, serverId).getConnectorMessages().get(0); - - Message message = null; - - message = channel.process(sourceMessage, false); - - // Assert that the message was run through the pre-processor - assertTrue(((TestPreProcessor) channel.getPreProcessor()).isProcessed()); - - // Assert that the processed raw content was stored - TestUtils.assertMessageContentExists(sourceMessage.getProcessedRaw()); - - // Assert that the FilterTransformer was run - assertTrue(((TestFilterTransformer) channel.getSourceConnector().getFilterTransformerExecutor().getFilterTransformer()).isTransformed()); - - // Assert that the transformed/encoded content was stored - TestUtils.assertMessageContentExists(sourceMessage.getTransformed()); - TestUtils.assertMessageContentExists(sourceMessage.getEncoded()); - - for (DestinationChainProvider chain : channel.getDestinationChainProviders()) { - Integer firstId = null; - for (Integer metaDataId : chain.getDestinationConnectors().keySet()) { - if (firstId == null || metaDataId < firstId) { - firstId = metaDataId; - } - } - // Assert that messages were created for each destination chain - TestUtils.assertConnectorMessageExists(message.getConnectorMessages().get(firstId), false); - // Assert that the message processed through at least the first destination connector for each chain - assertTrue(((TestDestinationConnector) chain.getDestinationConnectors().get(firstId)).getMessageIds().size() > 0); - } - - // Assert that the message was run through the post-processor - assertTrue(((TestPostProcessor) channel.getPostProcessor()).isProcessed()); - - channel.stop(); - channel.undeploy(); - } - - @Test - public final void testEncryption() throws Exception { - //TODO UPDATE THIS TEST! -// final String prefix = "Encrypted: "; -// final int prefixLength = prefix.length(); -// -// TestChannel channel = (TestChannel) TestUtils.createDefaultChannel(channelId, serverId); -// channel.setEncryptor(new Encryptor() { -// @Override -// public String encrypt(String text) { -// return prefix + text; -// } -// -// @Override -// public String decrypt(String text) { -// return text.substring(prefixLength); -// } -// }); -// -// SourceConnector sourceConnector = channel.getSourceConnector(); -// -// channel.deploy(); -// channel.start(); -// -// DispatchResult dispatchResult = sourceConnector.dispatchRawMessage(new RawMessage(testMessage)); -// sourceConnector.finishDispatch(dispatchResult); -// -// channel.stop(); -// channel.undeploy(); -// -// Connection connection = null; -// PreparedStatement statement = null; -// ResultSet resultSet = null; -// -// try { -// connection = TestUtils.getConnection(); -// -// long messageId = dispatchResult.getProcessedMessage().getMessageId(); -// -// statement = connection.prepareStatement("SELECT content, is_encrypted FROM d_mc" + ChannelController.getInstance().getLocalChannelId(channelId) + " WHERE message_id = ? AND metadata_id = ? AND content_type = ?"); -// statement.setLong(1, messageId); -// -// for (ConnectorMessage connectorMessage : dispatchResult.getProcessedMessage().getConnectorMessages().values()) { -// int metaDataId = connectorMessage.getMetaDataId(); -// statement.setInt(2, metaDataId); -// -// for (ContentType contentType : ContentType.getMessageTypes()) { -// MessageContent messageContent = connectorMessage.getContent(contentType); -// -// if (messageContent != null) { -// assertNotNull(messageContent.getContent()); -// //TODO Update this test, no longer valid -//// assertEquals(prefix + messageContent.getContent(), messageContent.getEncryptedContent()); -// } -// -// statement.setInt(3, contentType.getContentTypeCode()); -// resultSet = statement.executeQuery(); -// -// if (resultSet.next()) { -// assertEquals(prefix + messageContent.getContent(), resultSet.getString("content")); -// assertTrue(resultSet.getBoolean("is_encrypted")); -// } else if (messageContent != null && (metaDataId == 0 || !contentType.equals(ContentType.RAW))) { -// throw new AssertionError("Message content was not stored in the database (" + messageId + "/" + metaDataId + "/" + contentType.getContentTypeCode() + ")"); -// } -// -// resultSet.close(); -// } -// } -// } finally { -// TestUtils.close(resultSet); -// TestUtils.close(statement); -// TestUtils.close(connection); -// } - } - @Test - public final void testContentRemoval() throws Exception { - testContentRemoval(false, false); - testContentRemoval(true, false); - } - - @Test - public final void testContentRemovalWithQueueing() throws Exception { - testContentRemoval(false, true); - testContentRemoval(true, true); - } - - private void testContentRemoval(boolean removeContentOnCompletion, boolean useQueue) throws Exception { - TestChannel channel = (TestChannel) TestUtils.createDefaultChannel(channelId, serverId); - channel.getStorageSettings().setRemoveContentOnCompletion(removeContentOnCompletion); - - if (useQueue) { - DestinationConnectorProperties destinationConnectorProperties = ((DestinationConnectorPropertiesInterface) channel.getDestinationConnector(1).getConnectorProperties()).getDestinationConnectorProperties(); - destinationConnectorProperties.setQueueEnabled(true); - destinationConnectorProperties.setSendFirst(false); - } - - SourceConnector sourceConnector = channel.getSourceConnector(); - - channel.deploy(); - channel.start(null); - - DispatchResult dispatchResult = sourceConnector.dispatchRawMessage(new RawMessage(testMessage)); - sourceConnector.finishDispatch(dispatchResult); - - // if queueing, give the queue time to flush out - if (useQueue) { - Thread.sleep(1000); - } - - channel.stop(); - channel.undeploy(); - - for (ConnectorMessage connectorMessage : dispatchResult.getProcessedMessage().getConnectorMessages().values()) { - boolean foundContent = false; - - for (ContentType contentType : ContentType.getMessageTypes()) { - MessageContent messageContent = connectorMessage.getMessageContent(contentType); - - if (messageContent != null && (messageContent.getMetaDataId() == 0 || messageContent.getContentType() != ContentType.RAW)) { - foundContent = true; - - if (removeContentOnCompletion) { - TestUtils.assertMessageContentDoesNotExist(messageContent); - } else { - TestUtils.assertMessageContentExists(messageContent); - } - } - } - - assertTrue(foundContent); - } - } - - @Test - public final void testContentStorageDevelopment() throws Exception { - testContentStorageSettings(TestUtils.getStorageSettings(MessageStorageMode.DEVELOPMENT)); - } - - @Test - public final void testContentStorageProduction() throws Exception { - testContentStorageSettings(TestUtils.getStorageSettings(MessageStorageMode.PRODUCTION)); - } - - @Test - public final void testContentStorageMetadata() throws Exception { - testContentStorageSettings(TestUtils.getStorageSettings(MessageStorageMode.METADATA)); - } - - @Test - public final void testContentStorageDisabled() throws Exception { - testContentStorageSettings(TestUtils.getStorageSettings(MessageStorageMode.DISABLED)); - } - - private void testContentStorageSettings(StorageSettings storageSettings) throws Exception { - TestChannel channel = (TestChannel) TestUtils.createDefaultChannel(channelId, serverId); - channel.setStorageSettings(storageSettings); - SourceConnector sourceConnector = channel.getSourceConnector(); - - channel.deploy(); - channel.start(null); - - DispatchResult dispatchResult = sourceConnector.dispatchRawMessage(new RawMessage(testMessage)); - sourceConnector.finishDispatch(dispatchResult); - - channel.stop(); - channel.undeploy(); - - ConnectorMessage sourceMessage = dispatchResult.getProcessedMessage().getConnectorMessages().get(0); - ConnectorMessage destinationMessage = dispatchResult.getProcessedMessage().getConnectorMessages().get(1); - - assertNotNull(sourceMessage); - assertNotNull(destinationMessage); - - if (storageSettings.isStoreRaw()) { - TestUtils.assertMessageContentExists(sourceMessage.getRaw()); - } else { - TestUtils.assertMessageContentDoesNotExist(sourceMessage.getRaw()); - } - - if (storageSettings.isStoreProcessedRaw()) { - TestUtils.assertMessageContentExists(sourceMessage.getProcessedRaw()); - } else { - TestUtils.assertMessageContentDoesNotExist(sourceMessage.getProcessedRaw()); - } - - if (storageSettings.isStoreTransformed()) { - TestUtils.assertMessageContentExists(sourceMessage.getTransformed()); - TestUtils.assertMessageContentExists(destinationMessage.getTransformed()); - } else { - TestUtils.assertMessageContentDoesNotExist(sourceMessage.getTransformed()); - TestUtils.assertMessageContentDoesNotExist(destinationMessage.getTransformed()); - } - - if (storageSettings.isStoreSourceEncoded()) { - TestUtils.assertMessageContentExists(sourceMessage.getEncoded()); - } else { - TestUtils.assertMessageContentDoesNotExist(sourceMessage.getEncoded()); - } - - if (storageSettings.isStoreSent()) { - TestUtils.assertMessageContentExists(destinationMessage.getSent()); - } else { - TestUtils.assertMessageContentDoesNotExist(new MessageContent(channelId, dispatchResult.getMessageId(), 1, ContentType.SENT, null, null, false)); - } - - if (storageSettings.isStoreResponse()) { - TestUtils.assertMessageContentExists(destinationMessage.getResponse()); - } else { - TestUtils.assertMessageContentDoesNotExist(destinationMessage.getResponse()); - } - - if (storageSettings.isStoreResponseTransformed()) { - TestUtils.assertMessageContentExists(destinationMessage.getResponseTransformed()); - } else { - TestUtils.assertMessageContentDoesNotExist(destinationMessage.getResponseTransformed()); - } - - if (storageSettings.isStoreProcessedResponse()) { - TestUtils.assertMessageContentExists(destinationMessage.getProcessedResponse()); - } else { - TestUtils.assertMessageContentDoesNotExist(destinationMessage.getProcessedResponse()); - } - } + /* + * Replaced by the ci/tests/160-message-storage-levels fixtures (DEVELOPMENT, PRODUCTION, RAW + * and METADATA), MessageStorageDisabledTest (DISABLED) and the ci/tests/170-content-removal + * fixtures (removeContentOnCompletion, with and without a queued destination). + */ } diff --git a/donkey/src/test/java/com/mirth/connect/donkey/test/DestinationChainTests.java b/donkey/src/test/java/com/mirth/connect/donkey/test/DestinationChainTests.java deleted file mode 100644 index 903f94d8b6..0000000000 --- a/donkey/src/test/java/com/mirth/connect/donkey/test/DestinationChainTests.java +++ /dev/null @@ -1,264 +0,0 @@ -/* - * Copyright (c) Mirth Corporation. All rights reserved. - * - * http://www.mirthcorp.com - * - * The software in this package is published under the terms of the MPL license a copy of which has - * been included with this distribution in the LICENSE.txt file. - */ - -package com.mirth.connect.donkey.test; - -import static org.junit.Assert.assertTrue; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.util.Map; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; - -import com.mirth.connect.donkey.model.message.ConnectorMessage; -import com.mirth.connect.donkey.model.message.ContentType; -import com.mirth.connect.donkey.model.message.Response; -import com.mirth.connect.donkey.model.message.Status; -import com.mirth.connect.donkey.server.Donkey; -import com.mirth.connect.donkey.server.StartException; -import com.mirth.connect.donkey.server.channel.Channel; -import com.mirth.connect.donkey.server.channel.DestinationChainProvider; -import com.mirth.connect.donkey.server.channel.DispatchResult; -import com.mirth.connect.donkey.server.channel.FilterTransformerExecutor; -import com.mirth.connect.donkey.server.channel.FilterTransformerResult; -import com.mirth.connect.donkey.server.channel.MetaDataReplacer; -import com.mirth.connect.donkey.server.channel.components.FilterTransformerException; -import com.mirth.connect.donkey.server.controllers.ChannelController; -import com.mirth.connect.donkey.test.util.TestChannel; -import com.mirth.connect.donkey.test.util.TestConnectorProperties; -import com.mirth.connect.donkey.test.util.TestDataType; -import com.mirth.connect.donkey.test.util.TestDestinationConnector; -import com.mirth.connect.donkey.test.util.TestFilterTransformer; -import com.mirth.connect.donkey.test.util.TestPostProcessor; -import com.mirth.connect.donkey.test.util.TestPreProcessor; -import com.mirth.connect.donkey.test.util.TestResponseTransformer; -import com.mirth.connect.donkey.test.util.TestSourceConnector; -import com.mirth.connect.donkey.test.util.TestUtils; - -public class DestinationChainTests { - private static int TEST_SIZE = 10; - private static String channelId = TestUtils.DEFAULT_CHANNEL_ID; - private static String serverId = TestUtils.DEFAULT_SERVER_ID; - private static String testMessage = TestUtils.TEST_HL7_MESSAGE; - private Logger logger = LogManager.getLogger(this.getClass()); - - @BeforeClass - final public static void beforeClass() throws StartException { - Donkey.getInstance().startEngine(TestUtils.getDonkeyTestConfiguration()); - } - - @AfterClass - final public static void afterClass() throws StartException { - Donkey.getInstance().stopEngine(); - } - - /* - * Create channel with two destination chains, two destination connectors each Set each - * destination connector's FilterTransformer to place values in the connector, channel, and - * response maps - * - * Send messages, and for each destination connector, assert that: - The transformed data was - * stored - The encoded data was stored - The connector message maps were all updated correctly - * - The connector message status was updated - */ - @Test - public final void testStoreData() throws Exception { - int numChains = 2; - int numDestinationsPerChain = 2; - long localChannelId = ChannelController.getInstance().getLocalChannelId(channelId); - - TestChannel channel = new TestChannel(); - - channel.setChannelId(channelId); - channel.setServerId(serverId); - - channel.setPreProcessor(new TestPreProcessor()); - channel.setPostProcessor(new TestPostProcessor()); - - TestSourceConnector sourceConnector = (TestSourceConnector) TestUtils.createDefaultSourceConnector(); - sourceConnector.setRespondAfterProcessing(true); - sourceConnector.setChannelId(channel.getChannelId()); - sourceConnector.setChannel(channel); - sourceConnector.setMetaDataReplacer(new MetaDataReplacer()); - - channel.setSourceConnector(sourceConnector); - channel.getSourceConnector().setFilterTransformerExecutor(TestUtils.createDefaultFilterTransformerExecutor()); - - class TestFilterTransformer2 extends TestFilterTransformer { - @Override - public FilterTransformerResult doFilterTransform(ConnectorMessage message) throws FilterTransformerException { - // Alter the connector message maps - message.getConnectorMap().put("key", "value"); - message.getChannelMap().put("key", "value"); - message.getResponseMap().put("key", new Response(Status.SENT, "value")); - return super.doFilterTransform(message); - } - } - - for (int i = 1; i <= numChains; i++) { - DestinationChainProvider chain = new DestinationChainProvider(); - chain.setChannelId(channel.getChannelId()); - - for (int j = 1; j <= numDestinationsPerChain; j++) { - int metaDataId = (i - 1) * numDestinationsPerChain + j; - TestDestinationConnector destinationConnector = (TestDestinationConnector) TestUtils.createDestinationConnector(channel.getChannelId(), channel.getServerId(), new TestConnectorProperties(), TestUtils.DEFAULT_DESTINATION_NAME, new TestDataType(), new TestDataType(), new TestResponseTransformer(), metaDataId); - destinationConnector.setChannelId(channel.getChannelId()); - - destinationConnector.setMetaDataReplacer(sourceConnector.getMetaDataReplacer()); - destinationConnector.setMetaDataColumns(channel.getMetaDataColumns()); - - FilterTransformerExecutor filterTransformerExecutor = new FilterTransformerExecutor(new TestDataType(), new TestDataType()); - filterTransformerExecutor.setFilterTransformer(new TestFilterTransformer2()); - - destinationConnector.setFilterTransformerExecutor(filterTransformerExecutor); - - chain.addDestination(metaDataId, destinationConnector); - } - - channel.addDestinationChainProvider(chain); - } - - channel.deploy(); - channel.start(null); - - if (ChannelController.getInstance().channelExists(channelId)) { - ChannelController.getInstance().deleteAllMessages(channelId); - } - - for (int i = 1; i <= TEST_SIZE; i++) { - DispatchResult messageResponse = ((TestSourceConnector) channel.getSourceConnector()).readTestMessage(testMessage); - - for (DestinationChainProvider chain : channel.getDestinationChainProviders()) { - for (int metaDataId : chain.getDestinationConnectors().keySet()) { - Connection connection = null; - PreparedStatement statement = null; - ResultSet result = null; - - try { - connection = TestUtils.getConnection(); - - // Assert that the transformed data was stored - statement = connection.prepareStatement("SELECT * FROM d_mc" + localChannelId + " WHERE message_id = ? AND metadata_id = ? AND content_type = ?"); - statement.setLong(1, messageResponse.getMessageId()); - statement.setInt(2, metaDataId); - statement.setInt(3, ContentType.TRANSFORMED.getContentTypeCode()); - result = statement.executeQuery(); - assertTrue(result.next()); - TestUtils.close(result); - - // Assert that the encoded data was stored - statement.setInt(3, ContentType.ENCODED.getContentTypeCode()); - result = statement.executeQuery(); - assertTrue(result.next()); - } finally { - TestUtils.close(result); - TestUtils.close(statement); - TestUtils.close(connection); - } - - // Assert that the connector message maps were updated - Map connectorMap = TestUtils.getConnectorMap(channel.getChannelId(), messageResponse.getMessageId(), metaDataId); - Map channelMap = TestUtils.getChannelMap(channel.getChannelId(), messageResponse.getMessageId(), metaDataId); - Map responseMap = TestUtils.getResponseMap(channel.getChannelId(), messageResponse.getMessageId(), metaDataId); - assertTrue(connectorMap.get("key").equals("value")); - assertTrue(channelMap.get("key").equals("value")); - assertTrue(responseMap.get("key").equals(new Response(Status.SENT, "value"))); - - // Assert that the connector message status was updated - TestUtils.assertConnectorMessageStatusEquals(channel.getChannelId(), messageResponse.getMessageId(), metaDataId, Status.SENT); - } - } - } - - channel.stop(); - channel.undeploy(); - ChannelController.getInstance().removeChannel(channel.getChannelId()); - } - - /* - * Create channel with two destination chains, two destination connectors each Set the source - * FilterTransformer to place values in the channel and response maps - * - * Send messages, and for each destination connector, assert that: - The connector message was - * stored - The channel and response maps were updated correctly - If the destination connector - * isn't the first one in the chain, the raw data was stored - If the destination connector is - * the first one in the chain, the source encoded data was stored - */ - @Test - public final void testCreateNextMessage() throws Exception { - long localChannelId = ChannelController.getInstance().getLocalChannelId(channelId); - Channel channel = TestUtils.createDefaultChannel(channelId, serverId, true, 2, 2); - - channel.getSourceConnector().getFilterTransformerExecutor().setFilterTransformer(new TestFilterTransformer() { - @Override - public FilterTransformerResult doFilterTransform(ConnectorMessage message) throws FilterTransformerException { - // Alter the channel and response maps - message.getChannelMap().put("key", "value"); - message.getResponseMap().put("key", new Response(Status.SENT, "value")); - return new FilterTransformerResult(false, null); - } - }); - - channel.deploy(); - channel.start(null); - Connection connection = null; - PreparedStatement statement = null; - ResultSet result = null; - - try { - connection = TestUtils.getConnection(); - - for (int i = 1; i <= TEST_SIZE; i++) { - DispatchResult messageResponse = ((TestSourceConnector) channel.getSourceConnector()).readTestMessage(testMessage); - - for (DestinationChainProvider chain : channel.getDestinationChainProviders()) { - for (int metaDataId : chain.getMetaDataIds()) { - // Assert that the connector message was stored - statement = connection.prepareStatement("SELECT * FROM d_mm" + localChannelId + " WHERE message_id = ? AND id = ?"); - statement.setLong(1, messageResponse.getMessageId()); - statement.setInt(2, metaDataId); - result = statement.executeQuery(); - assertTrue(result.next()); - result.close(); - statement.close(); - - // Assert that the channel and response maps were updated - Map channelMap = TestUtils.getChannelMap(channel.getChannelId(), messageResponse.getMessageId(), metaDataId); - Map responseMap = TestUtils.getResponseMap(channel.getChannelId(), messageResponse.getMessageId(), metaDataId); - assertTrue(channelMap.get("key").equals("value")); - assertTrue(responseMap.get("key").equals(new Response(Status.SENT, "value"))); - - // Assert that the raw data was stored - statement = connection.prepareStatement("SELECT * FROM d_mc" + localChannelId + " WHERE message_id = ? AND metadata_id = 0 AND content_type = ?"); - statement.setLong(1, messageResponse.getMessageId()); - statement.setInt(2, ContentType.ENCODED.getContentTypeCode()); - result = statement.executeQuery(); - assertTrue(result.next()); - result.close(); - statement.close(); - } - } - } - } finally { - TestUtils.close(result); - TestUtils.close(statement); - TestUtils.close(connection); - } - - channel.stop(); - channel.undeploy(); - ChannelController.getInstance().removeChannel(channel.getChannelId()); - } -} diff --git a/donkey/src/test/java/com/mirth/connect/donkey/test/DestinationConnectorTests.java b/donkey/src/test/java/com/mirth/connect/donkey/test/DestinationConnectorTests.java index 1bac478ccd..a06ad092ed 100644 --- a/donkey/src/test/java/com/mirth/connect/donkey/test/DestinationConnectorTests.java +++ b/donkey/src/test/java/com/mirth/connect/donkey/test/DestinationConnectorTests.java @@ -16,26 +16,18 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; -import java.util.Map; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; -import com.mirth.connect.donkey.model.DonkeyException; import com.mirth.connect.donkey.model.channel.ConnectorProperties; import com.mirth.connect.donkey.model.channel.DeployedState; import com.mirth.connect.donkey.model.channel.DestinationConnectorProperties; -import com.mirth.connect.donkey.model.channel.DestinationConnectorPropertiesInterface; -import com.mirth.connect.donkey.model.message.ConnectorMessage; import com.mirth.connect.donkey.model.message.ContentType; -import com.mirth.connect.donkey.model.message.MessageContent; -import com.mirth.connect.donkey.model.message.RawMessage; -import com.mirth.connect.donkey.model.message.Response; import com.mirth.connect.donkey.model.message.Status; import com.mirth.connect.donkey.server.Donkey; import com.mirth.connect.donkey.server.StartException; -import com.mirth.connect.donkey.server.channel.Channel; import com.mirth.connect.donkey.server.channel.DestinationChainProvider; import com.mirth.connect.donkey.server.channel.DestinationConnector; import com.mirth.connect.donkey.server.channel.DispatchResult; @@ -47,10 +39,8 @@ import com.mirth.connect.donkey.test.util.TestDispatcherProperties; import com.mirth.connect.donkey.test.util.TestPostProcessor; import com.mirth.connect.donkey.test.util.TestPreProcessor; -import com.mirth.connect.donkey.test.util.TestResponseTransformer; import com.mirth.connect.donkey.test.util.TestSourceConnector; import com.mirth.connect.donkey.test.util.TestUtils; -import com.mirth.connect.donkey.util.Serializer; public class DestinationConnectorTests { private static int TEST_SIZE = 10; @@ -364,227 +354,8 @@ private void testProcess(boolean queueNull, boolean queueEnabled, boolean queueS } /* - * Create channel where the response transformer blocks the thread Send messages in asynchronous - * thread (so that the response transformer is waiting), assert that: - The destination - * connector response content was stored - The message status was updated to PENDING in the - * database - * - * Then allow the response transformer to finish, join the thread, and assert: - The response - * transformer was successfully run - The message status was updated to SENT in the database - */ - @Test - public final void testAfterSend() throws Exception { - ChannelController.getInstance().getLocalChannelId(channelId); - - final TestChannel channel = new TestChannel(); - - channel.setChannelId(channelId); - channel.setServerId(serverId); - - channel.setPreProcessor(new TestPreProcessor()); - channel.setPostProcessor(new TestPostProcessor()); - - final TestSourceConnector sourceConnector = (TestSourceConnector) TestUtils.createDefaultSourceConnector(); - sourceConnector.setChannelId(channel.getChannelId()); - sourceConnector.setChannel(channel); - channel.setSourceConnector(sourceConnector); - channel.getSourceConnector().setFilterTransformerExecutor(TestUtils.createDefaultFilterTransformerExecutor()); - - final ConnectorProperties connectorProperties = new TestDispatcherProperties(); - ((TestDispatcherProperties) connectorProperties).getDestinationConnectorProperties().setQueueEnabled(true); - ((TestDispatcherProperties) connectorProperties).getDestinationConnectorProperties().setSendFirst(true); - ((TestDispatcherProperties) connectorProperties).getDestinationConnectorProperties().setRegenerateTemplate(true); - - final DestinationConnector destinationConnector = new TestDispatcher(); - TestUtils.initDefaultDestinationConnector(destinationConnector, connectorProperties); - destinationConnector.setChannelId(channelId); - ((TestDispatcher) destinationConnector).setReturnStatus(Status.SENT); - - class BlockingTestResponseTransformer extends TestResponseTransformer { - public volatile boolean waiting = true; - - @Override - public String doTransform(Response response, ConnectorMessage connectorMessage) throws DonkeyException, InterruptedException { - while (waiting) { - try { - Thread.sleep(100); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - return super.doTransform(response, connectorMessage); - } - } - final BlockingTestResponseTransformer responseTransformer = new BlockingTestResponseTransformer(); - - destinationConnector.setMetaDataReplacer(sourceConnector.getMetaDataReplacer()); - destinationConnector.setMetaDataColumns(channel.getMetaDataColumns()); - destinationConnector.setFilterTransformerExecutor(TestUtils.createDefaultFilterTransformerExecutor()); - destinationConnector.setResponseTransformerExecutor(TestUtils.createDefaultResponseTransformerExecutor()); - destinationConnector.getResponseTransformerExecutor().setResponseTransformer(responseTransformer); - - DestinationChainProvider chain = new DestinationChainProvider(); - chain.setChannelId(channelId); - chain.addDestination(1, destinationConnector); - channel.addDestinationChainProvider(chain); - - if (ChannelController.getInstance().channelExists(channelId)) { - ChannelController.getInstance().deleteAllMessages(channelId); - } - - channel.deploy(); - channel.start(null); - - class TempClass { - public long messageId; - } - final TempClass tempClass = new TempClass(); - - for (int i = 1; i <= TEST_SIZE; i++) { - responseTransformer.waiting = true; - - Thread thread = new Thread() { - @Override - public void run() { - ConnectorMessage sourceMessage = TestUtils.createAndStoreNewMessage(new RawMessage(testMessage), channel.getChannelId(), channel.getName(), channel.getServerId()).getConnectorMessages().get(0); - tempClass.messageId = sourceMessage.getMessageId(); - - try { - channel.process(sourceMessage, false); - } catch (InterruptedException e) { - throw new AssertionError(e); - } - } - }; - thread.start(); - - Thread.sleep(100); - // Assert that the response content was stored - Connection connection = null; - PreparedStatement statement = null; - ResultSet result = null; - - try { - connection = TestUtils.getConnection(); - long localChannelId = ChannelController.getInstance().getLocalChannelId(channelId); - statement = connection.prepareStatement("SELECT * FROM d_mc" + localChannelId + " WHERE message_id = ? AND metadata_id = ? AND content_type = ?"); - statement.setLong(1, tempClass.messageId); - statement.setInt(2, 1); - statement.setInt(3, ContentType.SENT.getContentTypeCode()); - result = statement.executeQuery(); - assertTrue(result.next()); - result.close(); - statement.close(); - - // Assert that the message status was updated to PENDING - statement = connection.prepareStatement("SELECT * FROM d_mm" + localChannelId + " WHERE message_id = ? AND id = ? AND status = ?"); - statement.setLong(1, tempClass.messageId); - statement.setInt(2, 1); - statement.setString(3, String.valueOf(Status.PENDING.getStatusCode())); - result = statement.executeQuery(); - assertTrue(result.next()); - result.close(); - statement.close(); - - responseTransformer.waiting = false; - thread.join(); - - // Assert that the response transformer was run - assertTrue(responseTransformer.isTransformed()); - - // Assert that the message status was updated to SENT - statement = connection.prepareStatement("SELECT * FROM d_mm" + localChannelId + " WHERE message_id = ? AND id = ? AND status = ?"); - statement.setLong(1, tempClass.messageId); - statement.setInt(2, 1); - statement.setString(3, String.valueOf(Status.SENT.getStatusCode())); - result = statement.executeQuery(); - assertTrue(result.next()); - result.close(); - statement.close(); - } finally { - TestUtils.close(result); - TestUtils.close(statement); - TestUtils.close(connection); - } - } - - channel.stop(); - channel.undeploy(); - //ChannelController.getInstance().removeChannel(channel.getChannelId()); - } - - /* - * Create new channel where the response transformer changes the message and status of the - * Response object If the response status was changed to QUEUED and queuing is not enabled, or - * if the status was changed to something invalid (RECEIVED/TRANSFORMED/PENDING), then assume - * that it was changed to ERROR - * - * Send messages, assert that: - The processed response was stored - The destination entry in - * the response map was overwritten - The connector message status was changed based on the - * response status - * - * Do the above steps for all statuses + * testAfterSend and testRunResponseTransformer are re-implemented against the CI harness: + * ci/tests/190-response-handling for the response transformer, and + * smoketest BlockingResponseTransformerTest for a transformer that has not returned yet. */ - @Test - public final void testRunResponseTransformer() throws Exception { - for (Status status : Status.values()) { - testRunResponseTransformer(status); - } - } - - private void testRunResponseTransformer(Status responseStatus) throws Exception { - final Response testResponse = new Response(responseStatus, TestUtils.TEST_HL7_ACK); - Channel channel = TestUtils.createDefaultChannel(channelId, serverId); - - Response finalResponse = new Response(testResponse.getStatus(), testResponse.getMessage()); - if (finalResponse.getStatus() != Status.ERROR && finalResponse.getStatus() != Status.SENT && finalResponse.getStatus() != Status.QUEUED) { - // If the response is invalid for a final destination finalResponse.getStatus(), change the status to ERROR - finalResponse.setStatus(Status.ERROR); - } else if (channel.getDestinationConnector(1).getConnectorProperties() instanceof DestinationConnectorPropertiesInterface) { - // If the destination connector isn't queuing, and the response status is QUEUED, then it should have changed to ERROR - DestinationConnectorProperties destinationConnectorProperties = ((DestinationConnectorPropertiesInterface) channel.getDestinationConnector(1).getConnectorProperties()).getDestinationConnectorProperties(); - if ((destinationConnectorProperties == null || !destinationConnectorProperties.isQueueEnabled()) && finalResponse.getStatus() == Status.QUEUED) { - finalResponse.setStatus(Status.ERROR); - } - } else if (finalResponse.getStatus() == Status.QUEUED) { - // If the destination connector isn't queuing, and the response status is QUEUED, then it should have changed to ERROR - finalResponse.setStatus(Status.ERROR); - } - - class TestResponseTransformer2 extends TestResponseTransformer { - @Override - public String doTransform(Response response, ConnectorMessage connectorMessage) throws DonkeyException, InterruptedException { - response.setMessage(testResponse.getMessage()); - response.setStatus(testResponse.getStatus()); - connectorMessage.getResponseTransformed().setContent(testResponse.getMessage()); - return super.doTransform(response, connectorMessage); - } - } - channel.getDestinationConnector(1).getResponseTransformerExecutor().setResponseTransformer(new TestResponseTransformer2()); - - //ChannelController.getInstance().deleteAllMessages(channel.getChannelId()); - channel.deploy(); - channel.start(null); - - for (int i = 1; i <= TEST_SIZE; i++) { - DispatchResult messageResponse = ((TestSourceConnector) channel.getSourceConnector()).readTestMessage(testMessage); - Serializer serializer = Donkey.getInstance().getSerializer(); - String responseString = serializer.serialize(finalResponse); - - // Assert that the processed response was stored - MessageContent messageContent = new MessageContent(channel.getChannelId(), messageResponse.getMessageId(), 1, ContentType.PROCESSED_RESPONSE, responseString, null, false); - TestUtils.assertMessageContentExists(messageContent); - - // Assert that the entry in the response map was overwritten - Map responseMap = TestUtils.getResponseMap(channel.getChannelId(), messageResponse.getMessageId(), 1); - assertTrue(responseMap.get("d1").equals(finalResponse)); - - // Assert that the message status was changed - TestUtils.assertConnectorMessageStatusEquals(channel.getChannelId(), messageResponse.getMessageId(), 1, finalResponse.getStatus()); - } - - channel.stop(); - channel.undeploy(); - //ChannelController.getInstance().removeChannel(channel.getChannelId()); - } } diff --git a/donkey/src/test/java/com/mirth/connect/donkey/test/DonkeyDaoTests.java b/donkey/src/test/java/com/mirth/connect/donkey/test/DonkeyDaoTests.java index c7a4f8569c..087fa24a22 100644 --- a/donkey/src/test/java/com/mirth/connect/donkey/test/DonkeyDaoTests.java +++ b/donkey/src/test/java/com/mirth/connect/donkey/test/DonkeyDaoTests.java @@ -23,7 +23,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -32,8 +31,6 @@ import org.junit.BeforeClass; import org.junit.Test; -import com.mirth.connect.donkey.model.channel.MetaDataColumn; -import com.mirth.connect.donkey.model.channel.MetaDataColumnType; import com.mirth.connect.donkey.model.message.ConnectorMessage; import com.mirth.connect.donkey.model.message.ContentType; import com.mirth.connect.donkey.model.message.Message; @@ -41,12 +38,10 @@ import com.mirth.connect.donkey.model.message.RawMessage; import com.mirth.connect.donkey.model.message.Response; import com.mirth.connect.donkey.model.message.Status; -import com.mirth.connect.donkey.model.message.attachment.Attachment; import com.mirth.connect.donkey.server.Donkey; import com.mirth.connect.donkey.server.StartException; import com.mirth.connect.donkey.server.channel.Channel; import com.mirth.connect.donkey.server.channel.DestinationChainProvider; -import com.mirth.connect.donkey.server.channel.DispatchResult; import com.mirth.connect.donkey.server.channel.StorageSettings; import com.mirth.connect.donkey.server.controllers.ChannelController; import com.mirth.connect.donkey.server.data.DonkeyDao; @@ -324,103 +319,8 @@ public final void testInsertMessageContent() throws Exception { // TODO testBatchInsertMessageContent - /* - * Create new channel and message Insert attachments for the message, assert that: - The - * attachment was inserted correctly - */ - @Test - public final void testInsertMessageAttachment() throws Exception { - Channel channel = TestUtils.createDefaultChannel(channelId, serverId); - DonkeyDao dao = daoFactory.getDao(); - - try { - logger.info("Testing DonkeyDao.insertMessageAttachment..."); - - ConnectorMessage sourceMessage = TestUtils.createAndStoreNewMessage(new RawMessage(testMessage), channel.getChannelId(), channel.getName(), channel.getServerId(), daoFactory).getConnectorMessages().get(0); - - for (int i = 1; i <= TEST_SIZE; i++) { - Attachment attachment = new Attachment("attachment" + i, testMessage.getBytes(), "text/plain"); - - dao.insertMessageAttachment(channel.getChannelId(), sourceMessage.getMessageId(), attachment); - dao.commit(); - - // Assert that the attachment was inserted - TestUtils.assertAttachmentExists(channel.getChannelId(), sourceMessage.getMessageId(), attachment); - } - - System.out.println(daoTimer.getLog()); - } finally { - dao.close(); - } - } - - /* - * Create a new channel, and create metadata columns for it Deploy the channel, assert that: - - * The columns were successfully added to the table - * - * Create source connector messages Insert source/destination metadata for each message, assert - * that: - Each of the metadata columns was inserted successfully - */ - @Test - public final void testInsertMetaData() throws Exception { - Channel channel = TestUtils.createDefaultChannel(channelId, serverId); - - for (MetaDataColumnType columnType : MetaDataColumnType.values()) { - channel.getMetaDataColumns().add(new MetaDataColumn(columnType.toString() + "test", columnType, null)); - } - - channel.deploy(); - - // Assert that the columns were added successfully - List existingColumns = TestUtils.getExistingMetaDataColumns(channel.getChannelId()); - List channelColumns = channel.getMetaDataColumns(); - - assertEquals(channelColumns.size(), existingColumns.size()); - - for (MetaDataColumn metaDataColumn : channelColumns) { - assertTrue(metaDataColumn.getName(), existingColumns.contains(metaDataColumn)); - } - - Map sourceMap = new HashMap(); - sourceMap.put(MetaDataColumnType.BOOLEAN.toString() + "test", true); - sourceMap.put(MetaDataColumnType.NUMBER.toString() + "test", 1); - sourceMap.put(MetaDataColumnType.STRING.toString() + "test", "testing"); - sourceMap.put(MetaDataColumnType.TIMESTAMP.toString() + "test", Calendar.getInstance()); - - Map destinationMap = new HashMap(); - destinationMap.put(MetaDataColumnType.BOOLEAN.toString() + "test", false); - destinationMap.put(MetaDataColumnType.NUMBER.toString() + "test", 1); - destinationMap.put(MetaDataColumnType.STRING.toString() + "test", ""); - destinationMap.put(MetaDataColumnType.TIMESTAMP.toString() + "test", Calendar.getInstance()); - - logger.info("Testing DonkeyDao.insertMetaData..."); - - DonkeyDao dao = daoFactory.getDao(); - - try { - for (int i = 1; i <= TEST_SIZE; i++) { - ConnectorMessage sourceMessage = TestUtils.createAndStoreNewMessage(new RawMessage(testMessage), channel.getChannelId(), channel.getName(), channel.getServerId(), daoFactory).getConnectorMessages().get(0); - - sourceMessage.setMetaDataMap(sourceMap); - dao.insertMetaData(sourceMessage, channel.getMetaDataColumns()); - dao.commit(); - - ConnectorMessage destinationMessage = TestUtils.createAndStoreDestinationConnectorMessage(daoFactory, channel.getChannelId(), channel.getName(), channel.getServerId(), sourceMessage.getMessageId(), 1, testMessage, Status.RECEIVED); - destinationMessage.setMetaDataMap(destinationMap); - dao.insertMetaData(destinationMessage, channel.getMetaDataColumns()); - dao.commit(); - - // Assert the custom metadata was inserted correctly - TestUtils.compareMetaDataMaps(channel.getMetaDataColumns(), sourceMap, TestUtils.getCustomMetaData(channel.getChannelId(), sourceMessage.getMessageId(), 0)); - TestUtils.compareMetaDataMaps(channel.getMetaDataColumns(), destinationMap, TestUtils.getCustomMetaData(channel.getChannelId(), sourceMessage.getMessageId(), 1)); - } - - System.out.println(daoTimer.getLog()); - } finally { - dao.close(); - channel.undeploy(); - } - } + // Attachment storage and retrieval is covered against every dialect by + // ci/tests/230-attachments. /* * Create new channel and connector messages Insert content for the messages and assert that: - @@ -704,263 +604,9 @@ public final void testMarkAsProcessed() throws Exception { // TODO testResetMessage - /* - * Create a new channel Process messages directly through Channel.process, assert that: - Each - * message was inserted successfully - TEST_SIZE messages were added to the testing list - The - * channel statistics were updated correctly - * - * Iterate through each message in the list, assert that: - Each message, connector message, and - * message content row was deleted - The channel statistics were not deleted - * - * Do the same thing as above, except this time delete the channel statistics as well, assert - * that: - The channel statistics were deleted - */ - @Test - public void testDeleteMessage() throws Exception { // FIXME - TestChannel channel = TestUtils.createDefaultChannel(channelId, serverId); - - channel.deploy(); - channel.start(null); - - DonkeyDao dao = daoFactory.getDao(); - - try { - logger.info("Testing DonkeyDao.deleteMessage..."); - - // Test deleting messages without deleting statistics - testDeleteMessage(channel, false, dao); - - // Test deleting messages with deleting statistics - testDeleteMessage(channel, true, dao); - } finally { - dao.close(); - channel.stop(); - channel.undeploy(); - } - } - - private void testDeleteMessage(TestChannel channel, boolean deleteStatistics, DonkeyDao dao) throws Exception { - List messages = new ArrayList(); - TestUtils.deleteChannelStatistics(channel.getChannelId()); - - // Process a bunch of messages through the channel - for (int i = 1; i <= TEST_SIZE; i++) { - ConnectorMessage sourceMessage = TestUtils.createAndStoreNewMessage(new RawMessage(testMessage), channel.getChannelId(), channel.getName(), channel.getServerId(), daoFactory).getConnectorMessages().get(0); - - // Bypass the source connector so we can retrieve the Message object - Message message = channel.process(sourceMessage, true); - - // Assert that each message was successfully created - TestUtils.assertMessageExists(message, true); - messages.add(message); - } - - // Assert that TEST_SIZE messages were added - assertEquals(TEST_SIZE, messages.size()); - - Map> channelStats = ChannelController.getInstance().getStatistics().getChannelStats(channelId); - - try { - // Assert that the statistics were updated, ignore the RECEIVED status for the source/aggregate - assertNotNull(channelStats); - assertNotNull(channelStats.get(null)); - assertNotNull(channelStats.get(null).get(Status.SENT)); - assertNotNull(channelStats.get(0)); - assertNotNull(channelStats.get(1)); - assertNotNull(channelStats.get(1).get(Status.RECEIVED)); - assertNotNull(channelStats.get(1).get(Status.SENT)); - - assertEquals(TEST_SIZE, channelStats.get(null).get(Status.SENT).intValue()); - assertEquals(TEST_SIZE, channelStats.get(1).get(Status.RECEIVED).intValue()); - assertEquals(TEST_SIZE, channelStats.get(1).get(Status.SENT).intValue()); - } catch (AssertionError e) { - for (Entry> entry : channelStats.entrySet()) { - System.out.printf("metaDataId %-5s: %s\n", entry.getKey(), entry.getValue()); - } - - throw e; - } - - // Delete all the messages that were processed - for (Message message : messages) { - if (deleteStatistics) { - dao.deleteMessageStatistics(message.getChannelId(), message.getMessageId(), null); - } - - dao.deleteMessage(message.getChannelId(), message.getMessageId()); - dao.commit(); - - // Assert that each message was successfully deleted - for (ConnectorMessage connectorMessage : message.getConnectorMessages().values()) { - for (ContentType contentType : ContentType.getMessageTypes()) { - if (connectorMessage.getMessageContent(contentType) != null) { - // Assert that each content row was deleted - TestUtils.assertMessageContentDoesNotExist(connectorMessage.getMessageContent(contentType)); - } - } - - // Assert that each metadata row was deleted - TestUtils.assertConnectorMessageDoesNotExist(connectorMessage); - } - - // Assert that the message row itself was deleted - TestUtils.assertMessageDoesNotExist(message); - } - - if (deleteStatistics) { - // Assert that the statistics were decremented - channelStats = ChannelController.getInstance().getStatistics().getChannelStats(channelId); - assertEquals(0, channelStats.get(null).get(Status.RECEIVED).intValue()); - assertEquals(0, channelStats.get(null).get(Status.SENT).intValue()); - assertEquals(0, channelStats.get(0).get(Status.RECEIVED).intValue()); - assertEquals(0, channelStats.get(1).get(Status.RECEIVED).intValue()); - assertEquals(0, channelStats.get(1).get(Status.SENT).intValue()); - } else { - // Assert that the statistics were not deleted, ignore the RECEIVED status for the source/aggregate - channelStats = ChannelController.getInstance().getStatistics().getChannelStats(channelId); - assertEquals(TEST_SIZE, channelStats.get(null).get(Status.SENT).intValue()); - assertEquals(TEST_SIZE, channelStats.get(1).get(Status.RECEIVED).intValue()); - assertEquals(TEST_SIZE, channelStats.get(1).get(Status.SENT).intValue()); - } - } - - /* - * Deploy a new channel, process messages For each message, assert that: - Each connector - * message and content was inserted - * - * Then delete the connector messages and assert: - Each connector message and content was - * deleted - */ - @Test - public final void testDeleteConnectorMessages() throws Exception { - TestChannel channel = TestUtils.createDefaultChannel(channelId, serverId); - - channel.deploy(); - channel.start(null); - - try { - logger.info("Testing DonkeyDao.deleteConnectorMessages..."); - - for (int i = 1; i <= TEST_SIZE; i++) { - Message message = TestUtils.createAndStoreNewMessage(new RawMessage(testMessage), channel.getChannelId(), channel.getName(), channel.getServerId(), daoFactory); - channel.process(message.getConnectorMessages().get(0), true); - - for (ConnectorMessage connectorMessage : message.getConnectorMessages().values()) { - TestUtils.assertConnectorMessageExists(connectorMessage, true); - } - - DonkeyDao dao = null; - - try { - dao = daoFactory.getDao(); - dao.deleteConnectorMessages(channel.getChannelId(), message.getMessageId(), message.getConnectorMessages().keySet()); - dao.commit(); - } finally { - TestUtils.close(dao); - } - - for (ConnectorMessage connectorMessage : message.getConnectorMessages().values()) { - for (ContentType contentType : ContentType.getMessageTypes()) { - MessageContent messageContent = connectorMessage.getMessageContent(contentType); - if (messageContent != null) { - TestUtils.assertMessageContentDoesNotExist(messageContent); - } - } - TestUtils.assertConnectorMessageDoesNotExist(connectorMessage); - } - } - - System.out.println(daoTimer.getLog()); - } finally { - channel.stop(); - channel.undeploy(); - } - } - - // TODO testDeleteMessageContent - - // TODO testDeleteMessageAttachments - - // TODO testDeleteMessageStatistics - - /* - * Deploy a new channel, process messages Delete all messages for the channel and assert: - The - * message table was truncated - The message metadata table was truncated - The message content - * table was truncated - The message custom metadata table was truncated - The message - * attachment table was truncated - */ - @Test - public final void testDeleteAllMessages() throws Exception { - TestChannel channel = TestUtils.createDefaultChannel(channelId, serverId); - - channel.deploy(); - channel.start(null); - - logger.info("Testing DonkeyDao.deleteAllMessages..."); - - for (int i = 1; i <= TEST_SIZE; i++) { - ((TestSourceConnector) channel.getSourceConnector()).readTestMessage(testMessage); - } - - DonkeyDao dao = null; - - try { - dao = daoFactory.getDao(); - dao.deleteAllMessages(channel.getChannelId()); - dao.commit(); - } finally { - TestUtils.close(dao); - } - - Connection connection = null; - PreparedStatement statement = null; - ResultSet result = null; - - try { - // Assert that all the message tables have been truncated - long localChannelId = ChannelController.getInstance().getLocalChannelId(channel.getChannelId()); - connection = TestUtils.getConnection(); - - statement = connection.prepareStatement("SELECT * FROM d_m" + localChannelId); - result = statement.executeQuery(); - assertFalse(result.next()); - result.close(); - statement.close(); - - statement = connection.prepareStatement("SELECT * FROM d_mm" + localChannelId); - result = statement.executeQuery(); - assertFalse(result.next()); - result.close(); - statement.close(); - - statement = connection.prepareStatement("SELECT * FROM d_mc" + localChannelId); - result = statement.executeQuery(); - assertFalse(result.next()); - result.close(); - statement.close(); - - statement = connection.prepareStatement("SELECT * FROM d_mcm" + localChannelId); - result = statement.executeQuery(); - assertFalse(result.next()); - result.close(); - statement.close(); - - statement = connection.prepareStatement("SELECT * FROM d_ma" + localChannelId); - result = statement.executeQuery(); - assertFalse(result.next()); - result.close(); - statement.close(); - - System.out.println(daoTimer.getLog()); - } finally { - TestUtils.close(result); - TestUtils.close(statement); - TestUtils.close(connection); - - channel.stop(); - channel.undeploy(); - } - } + // Deleting one message, one connector message, or every message in a channel - and the + // cascade into content, attachments, custom metadata and statistics that each one has to + // carry out - is covered against every dialect by the 240-message-deletion smoke tests. /* * Use createChannel to create some new channels; assert: - The channel ID and local channel ID @@ -1127,102 +773,6 @@ public final void testRemoveChannel() throws Exception { System.out.println(daoTimer.getLog()); } - /* - * Deploy a new channel, manually add metadata columns using addMetaDataColumn, and assert that: - * - All the columns were successfully added - */ - @Test - public final void testAddMetaDataColumn() throws Exception { - Channel channel = TestUtils.createDefaultChannel(channelId, serverId); - - List metaDataColumns = new ArrayList(); - - try { - logger.info("Testing DonkeyDao.addMetaDataColumn..."); - - channel.deploy(); - - for (int i = 1; i <= TEST_SIZE; i++) { - DonkeyDao dao = null; - - try { - dao = daoFactory.getDao(); - - for (MetaDataColumnType type : MetaDataColumnType.values()) { - MetaDataColumn metaDataColumn = new MetaDataColumn(type.toString() + "column" + i, type, null); - dao.addMetaDataColumn(channel.getChannelId(), metaDataColumn); - metaDataColumns.add(metaDataColumn); - } - - logger.debug("Adding metadata column set " + i); - dao.commit(); - } finally { - TestUtils.close(dao); - } - - // Assert that the columns were added - assertEquals(metaDataColumns, TestUtils.getExistingMetaDataColumns(channel.getChannelId())); - } - - System.out.println(daoTimer.getLog()); - } finally { - channel.undeploy(); - } - } - - /* - * Deploy a new channel, add metadata columns, then use removeMetaDataColumn to delete all the - * columns added Get the list of existing metadata columns in the database, and assert: - All - * the columns previously added were successfully removed - */ - @Test - public final void testRemoveMetaDataColumn() throws Exception { - Channel channel = TestUtils.createDefaultChannel(channelId, serverId); - - List metaDataColumns = new ArrayList(); - - try { - logger.info("Testing DonkeyDao.addMetaDataColumn..."); - - channel.deploy(); - DonkeyDao dao = null; - - try { - dao = daoFactory.getDao(); - - for (int i = 1; i <= TEST_SIZE; i++) { - for (MetaDataColumnType type : MetaDataColumnType.values()) { - MetaDataColumn metaDataColumn = new MetaDataColumn(type.toString() + "column" + i, type, null); - dao.addMetaDataColumn(channel.getChannelId(), metaDataColumn); - metaDataColumns.add(metaDataColumn); - } - } - - dao.commit(); - - // Remove the columns - for (MetaDataColumn metaDataColumn : metaDataColumns) { - dao.removeMetaDataColumn(channel.getChannelId(), metaDataColumn.getName()); - } - - dao.commit(); - } finally { - TestUtils.close(dao); - } - - List databaseMetaDataColumns = TestUtils.getExistingMetaDataColumns(channel.getChannelId()); - - // Assert that the columns in the database do not contain any of the columns previously added - for (MetaDataColumn metaDataColumn : metaDataColumns) { - assertFalse(databaseMetaDataColumns.contains(metaDataColumn)); - } - - System.out.println(daoTimer.getLog()); - } finally { - channel.undeploy(); - } - } - // TODO testResetStatistics // TODO testResetAllStatistics @@ -1761,92 +1311,10 @@ public final void testGetUnfinishedMessages() throws Exception { } } - /* - * Create a list of metadata columns and add the list to the channel's metadata columns Deploy - * the channel, and assert that: - The list of metadata columns matches the one returned by - * getMetaDataColumns - */ - @Test - public final void testGetMetaDataColumns() throws Exception { - Channel channel = TestUtils.createDefaultChannel(channelId, serverId); - - List metaDataColumns = new ArrayList(); - for (MetaDataColumnType type : MetaDataColumnType.values()) { - metaDataColumns.add(new MetaDataColumn(type.toString() + "column", type, null)); - } - channel.setMetaDataColumns(metaDataColumns); - - try { - logger.info("Testing DonkeyDao.getMetaDataColumns..."); - - channel.deploy(); - - List daoMetaDataColumns; - DonkeyDao dao = null; - - try { - dao = daoFactory.getDao(); - daoMetaDataColumns = dao.getMetaDataColumns(channel.getChannelId()); - } finally { - TestUtils.close(dao); - } - - assertEquals(metaDataColumns.size(), daoMetaDataColumns.size()); - - for (MetaDataColumn column : daoMetaDataColumns) { - assertTrue(column.getName(), metaDataColumns.contains(column)); - } - - System.out.println(daoTimer.getLog()); - } finally { - channel.undeploy(); - } - } - // TODO testGetMessageAttachment - /* - * Start up a new channel, assert that: - The channel statistics in the database are the same as - * the ones returned from getChannelStatistics - * - * Then send messages, and after each one assert: - The channel statistics in the database are - * the same as the ones returned from getChannelStatistics - */ - @Test - public final void testGetChannelStatistics() throws Exception { - // TODO also test getChannelTotalStatistics here - - Channel channel = TestUtils.createDefaultChannel(channelId, serverId); - channel.deploy(); - channel.start(null); - - DispatchResult dispatchResult = null; - - try { - dispatchResult = channel.getSourceConnector().dispatchRawMessage(new RawMessage(TestUtils.TEST_HL7_MESSAGE)); - } finally { - channel.getSourceConnector().finishDispatch(dispatchResult); - } - - try { - logger.info("Testing DonkeyDao.getChannelStatistics..."); - - // Assert that the statistics are correct - assertEquals(TestUtils.getChannelStatistics(channel.getChannelId()), ChannelController.getInstance().getStatistics().getChannelStats(channel.getChannelId())); - - for (int i = 1; i <= TEST_SIZE; i++) { - ((TestSourceConnector) channel.getSourceConnector()).readTestMessage(testMessage); - - // Assert that the statistics are correct - assertEquals(TestUtils.getChannelStatistics(channel.getChannelId()), ChannelController.getInstance().getStatistics().getChannelStats(channel.getChannelId())); - } - - System.out.println(daoTimer.getLog()); - } finally { - channel.stop(); - channel.undeploy(); - } - } + // Reading a channel's statistics back - and the per-dialect statements that wrote them + // there - is covered against every dialect by the 250-statistics smoke tests. /** * Sends messages through 5 channels with (maxConnections * 2) asynchronous destinations for 10 @@ -1916,4 +1384,11 @@ public final void testJdbcDaoStatementCache() throws Exception { // channel.undeploy(); // } // } + /* + * Replaced by the ci/tests/200-custom-metadata-columns fixtures, which deploy a channel with a + * column of each type and assert the values the engine stored and read back, and by + * CustomMetaDataColumnRedeployTest, which edits a deployed channel's column list to reach + * removeMetaDataColumn. Covered there: testInsertMetaData, testAddMetaDataColumn, + * testRemoveMetaDataColumn and testGetMetaDataColumns. + */ } diff --git a/donkey/src/test/java/com/mirth/connect/donkey/test/FilterTransformerTests.java b/donkey/src/test/java/com/mirth/connect/donkey/test/FilterTransformerTests.java deleted file mode 100644 index fe9ad10219..0000000000 --- a/donkey/src/test/java/com/mirth/connect/donkey/test/FilterTransformerTests.java +++ /dev/null @@ -1,305 +0,0 @@ -/* - * Copyright (c) Mirth Corporation. All rights reserved. - * - * http://www.mirthcorp.com - * - * The software in this package is published under the terms of the MPL license a copy of which has - * been included with this distribution in the LICENSE.txt file. - */ - -package com.mirth.connect.donkey.test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; - -import java.util.Calendar; -import java.util.Map; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; - -import com.mirth.connect.donkey.model.DonkeyException; -import com.mirth.connect.donkey.model.message.ConnectorMessage; -import com.mirth.connect.donkey.model.message.ContentType; -import com.mirth.connect.donkey.model.message.MessageContent; -import com.mirth.connect.donkey.model.message.MessageSerializer; -import com.mirth.connect.donkey.model.message.MessageSerializerException; -import com.mirth.connect.donkey.model.message.Status; -import com.mirth.connect.donkey.server.Donkey; -import com.mirth.connect.donkey.server.StartException; -import com.mirth.connect.donkey.server.channel.FilterTransformerExecutor; -import com.mirth.connect.donkey.server.channel.FilterTransformerResult; -import com.mirth.connect.donkey.server.channel.components.FilterTransformerException; -import com.mirth.connect.donkey.server.message.DataType; -import com.mirth.connect.donkey.test.util.TestAutoResponder; -import com.mirth.connect.donkey.test.util.TestDataType; -import com.mirth.connect.donkey.test.util.TestFilterTransformer; -import com.mirth.connect.donkey.test.util.TestSerializer; -import com.mirth.connect.donkey.test.util.TestUtils; - -public class FilterTransformerTests { - private static int TEST_SIZE = 10; - private static String channelId = TestUtils.DEFAULT_CHANNEL_ID; - private static String channelName = TestUtils.DEFAULT_CHANNEL_ID; - private static String serverId = TestUtils.DEFAULT_SERVER_ID; - private static String testMessage = TestUtils.TEST_HL7_MESSAGE; - - private Logger logger = LogManager.getLogger(this.getClass()); - - @BeforeClass - final public static void beforeClass() throws StartException { - Donkey.getInstance().startEngine(TestUtils.getDonkeyTestConfiguration()); - } - - @AfterClass - final public static void afterClass() throws StartException { - Donkey.getInstance().stopEngine(); - } - - /* - * For each test, create a new connector message with some raw content - * - * Set the inbound data type to a FailingTestDataType which always throws a SerializerException - * Process the connector message, and assert that: - The status is ERROR - The transformed - * content is null - The encoded content is null - * - * Set the filter/transformer interface to one that always returns false Process the connector - * message, and assert that: - The status is FILTERED - The transformed content is not null - - * The encoded content is null - * - * Set the filter/transformer interface to one that always throws a FilterTransformerException - * Process the connector message, and assert that: - The status is ERROR - The transformed - * content is not null - The encoded content is null - * - * Set the processed raw content on the connector message Process the connector message, and - * assert that: - The status is TRANSFORMED - The transformed content is not null - The encoded - * content is not null - The transformed content is equal to the serialized processed raw - * content - The encoded content is equal to the serialized/deserialized processed raw content - */ - @Test - public void testWithFilterTransformer() throws Exception { - ConnectorMessage connectorMessage; - FilterTransformerExecutor filterTransformerExecutor; - - class FailingTestSerializer implements MessageSerializer { - @Override - public boolean isSerializationRequired(boolean isXml) { - return false; - } - - @Override - public String transformWithoutSerializing(String message, MessageSerializer outboundSerializer) { - return message; - } - - @Override - public String toXML(String message) throws MessageSerializerException { - throw new MessageSerializerException("Inbound serialization failed."); - } - - @Override - public String fromXML(String message) throws MessageSerializerException { - throw new MessageSerializerException("Outbound serialization failed."); - } - - @Override - public void populateMetaData(String message, Map map) {} - - @Override - public String toJSON(String message) throws MessageSerializerException { - return null; - } - - @Override - public String fromJSON(String message) throws MessageSerializerException { - return null; - } - } - - class FailingTestDataType extends DataType { - public FailingTestDataType() { - super("HL7V2", new FailingTestSerializer(), new TestAutoResponder()); - } - } - - logger.info("Testing FilterTransformerExecutor.processConnectorMessage with a FilterTransformer..."); - - /* - * Assert that if inbound serialization failed, the status is set to ERROR, and the - * transformed/encoded content is not set - */ - filterTransformerExecutor = new FilterTransformerExecutor(new FailingTestDataType(), new TestDataType()); - filterTransformerExecutor.setFilterTransformer(new TestFilterTransformer()); - for (int i = 1; i <= TEST_SIZE; i++) { - connectorMessage = new ConnectorMessage(channelId, channelName, 1, 1, serverId, Calendar.getInstance(), Status.RECEIVED); - connectorMessage.setRaw(new MessageContent(channelId, 1, 1, ContentType.RAW, testMessage, "HL7V2", false)); - - try { - filterTransformerExecutor.processConnectorMessage(connectorMessage); - } catch (MessageSerializerException e) { - } - - assertNull(connectorMessage.getTransformed()); - assertNull(connectorMessage.getEncoded()); - } - - /* - * Assert that if the message is filtered, the status is set to FILTERED, the transformed - * content is set, and the encoded content is not set - */ - filterTransformerExecutor = new FilterTransformerExecutor(new TestDataType(), new TestDataType()); - filterTransformerExecutor.setFilterTransformer(new TestFilterTransformer() { - @Override - public FilterTransformerResult doFilterTransform(ConnectorMessage message) throws FilterTransformerException { - return new FilterTransformerResult(true, null); - } - }); - for (int i = 1; i <= TEST_SIZE; i++) { - connectorMessage = new ConnectorMessage(channelId, channelName, 1, 1, serverId, Calendar.getInstance(), Status.RECEIVED); - connectorMessage.setRaw(new MessageContent(channelId, 1, 1, ContentType.RAW, testMessage, "HL7V2", false)); - - try { - filterTransformerExecutor.processConnectorMessage(connectorMessage); - } catch (FilterTransformerException e) { - } - - assertEquals(Status.FILTERED, connectorMessage.getStatus()); - assertNotNull(connectorMessage.getTransformed()); - assertNull(connectorMessage.getEncoded()); - } - - /* - * Assert that if the filter/transformer interface throws an exception, then the status is - * set to ERROR, the transformed content is set, and the encoded content is not set - */ - filterTransformerExecutor = new FilterTransformerExecutor(new TestDataType(), new TestDataType()); - filterTransformerExecutor.setFilterTransformer(new TestFilterTransformer() { - @Override - public FilterTransformerResult doFilterTransform(ConnectorMessage message) throws FilterTransformerException { - throw new FilterTransformerException("Failed to run filter/transformer.", new Exception(), null); - } - }); - for (int i = 1; i <= TEST_SIZE; i++) { - connectorMessage = new ConnectorMessage(channelId, channelName, 1, 1, serverId, Calendar.getInstance(), Status.RECEIVED); - connectorMessage.setRaw(new MessageContent(channelId, 1, 1, ContentType.RAW, testMessage, "HL7V2", false)); - - try { - filterTransformerExecutor.processConnectorMessage(connectorMessage); - } catch (FilterTransformerException e) { - } - - assertNotNull(connectorMessage.getTransformed()); - assertNull(connectorMessage.getEncoded()); - } - - /* - * Assert that if the outbound deserialization fails, then the status is set to ERROR, the - * transformed content is set, and the encoded content is not set - */ - filterTransformerExecutor = new FilterTransformerExecutor(new TestDataType(), new FailingTestDataType()); - filterTransformerExecutor.setFilterTransformer(new TestFilterTransformer()); - for (int i = 1; i <= TEST_SIZE; i++) { - connectorMessage = new ConnectorMessage(channelId, channelName, 1, 1, serverId, Calendar.getInstance(), Status.RECEIVED); - connectorMessage.setRaw(new MessageContent(channelId, 1, 1, ContentType.RAW, testMessage, "HL7V2", false)); - - try { - filterTransformerExecutor.processConnectorMessage(connectorMessage); - } catch (DonkeyException e) { - } - - assertNotNull(connectorMessage.getTransformed()); - assertNull(connectorMessage.getEncoded()); - } - - /* - * Assert that if everything runs without errors, then the status is set to TRANSFORMED, and - * the transformed/encoded content is set - */ - filterTransformerExecutor = new FilterTransformerExecutor(new TestDataType(), new TestDataType()); - filterTransformerExecutor.setFilterTransformer(new TestFilterTransformer()); - for (int i = 1; i <= TEST_SIZE; i++) { - connectorMessage = new ConnectorMessage(channelId, channelName, 1, 1, serverId, Calendar.getInstance(), Status.RECEIVED); - connectorMessage.setRaw(new MessageContent(channelId, 1, 1, ContentType.RAW, testMessage, "HL7V2", false)); - filterTransformerExecutor.processConnectorMessage(connectorMessage); - - assertEquals(Status.TRANSFORMED, connectorMessage.getStatus()); - assertNotNull(connectorMessage.getTransformed()); - assertNotNull(connectorMessage.getEncoded()); - } - - /* - * Assert that if the processed raw content is set, then the status is set to TRANSFORMED, - * the transformed/encoded content is set, and the transformed content is the serialized - * processed raw content rather than the raw content - */ - filterTransformerExecutor = new FilterTransformerExecutor(new TestDataType(), new TestDataType()); - filterTransformerExecutor.setFilterTransformer(new TestFilterTransformer()); - for (int i = 1; i <= TEST_SIZE; i++) { - connectorMessage = new ConnectorMessage(channelId, channelName, 1, 1, serverId, Calendar.getInstance(), Status.RECEIVED); - connectorMessage.setRaw(new MessageContent(channelId, 1, 1, ContentType.RAW, "", "HL7V2", false)); - connectorMessage.setProcessedRaw(new MessageContent(channelId, 1, 1, ContentType.PROCESSED_RAW, testMessage, "HL7V2", false)); - filterTransformerExecutor.processConnectorMessage(connectorMessage); - - assertEquals(Status.TRANSFORMED, connectorMessage.getStatus()); - assertNotNull(connectorMessage.getTransformed()); - assertNotNull(connectorMessage.getEncoded()); - assertEquals((new TestSerializer()).toXML(testMessage), connectorMessage.getTransformed().getContent()); - assertEquals((new TestSerializer()).fromXML((new TestSerializer()).toXML(testMessage)), connectorMessage.getEncoded().getContent()); - } - } - - /* - * For each test, create a new connector message with some raw content - * - * Process the connector message, and assert that: - The status is TRANSFORMED - The transformed - * content is null - The encoded content is equal to the raw content - * - * Set the processed raw content on the connector message Process the connector message, and - * assert that: - The status is TRANSFORMED - The transformed content is null - The encoded - * content is equal to the processed raw content - */ - @Test - public void testWithoutFilterTransformer() throws Exception { - ConnectorMessage connectorMessage; - FilterTransformerExecutor filterTransformerExecutor; - - logger.info("Testing FilterTransformerExecutor.processConnectorMessage without a FilterTransformer..."); - - /* - * Assert that if the processed raw content is not set, then the status is set to - * TRANSFORMED, the transformed content is not set, and the encoded content is set to the - * raw content - */ - filterTransformerExecutor = new FilterTransformerExecutor(new TestDataType(), new TestDataType()); - for (int i = 1; i <= TEST_SIZE; i++) { - connectorMessage = new ConnectorMessage(channelId, channelName, 1, 1, serverId, Calendar.getInstance(), Status.RECEIVED); - connectorMessage.setRaw(new MessageContent(channelId, 1, 1, ContentType.RAW, testMessage, "HL7V2", false)); - filterTransformerExecutor.processConnectorMessage(connectorMessage); - - assertEquals(Status.TRANSFORMED, connectorMessage.getStatus()); - assertNull(connectorMessage.getTransformed()); - assertEquals(testMessage, connectorMessage.getEncoded().getContent()); - } - - /* - * Assert that if the processed raw content is set, then the status is set to TRANSFORMED, - * the transformed content is not set, the encoded content is set to the processed raw - * content rather than the raw content - */ - filterTransformerExecutor = new FilterTransformerExecutor(new TestDataType(), new TestDataType()); - for (int i = 1; i <= TEST_SIZE; i++) { - connectorMessage = new ConnectorMessage(channelId, channelName, 1, 1, serverId, Calendar.getInstance(), Status.RECEIVED); - connectorMessage.setRaw(new MessageContent(channelId, 1, 1, ContentType.RAW, "", "HL7V2", false)); - connectorMessage.setProcessedRaw(new MessageContent(channelId, 1, 1, ContentType.PROCESSED_RAW, testMessage, "HL7V2", false)); - filterTransformerExecutor.processConnectorMessage(connectorMessage); - - assertEquals(Status.TRANSFORMED, connectorMessage.getStatus()); - assertNull(connectorMessage.getTransformed()); - assertEquals(testMessage, connectorMessage.getEncoded().getContent()); - } - } -} diff --git a/donkey/src/test/java/com/mirth/connect/donkey/test/MessageControllerTests.java b/donkey/src/test/java/com/mirth/connect/donkey/test/MessageControllerTests.java index e9dce58d9e..b4f3d60ef0 100644 --- a/donkey/src/test/java/com/mirth/connect/donkey/test/MessageControllerTests.java +++ b/donkey/src/test/java/com/mirth/connect/donkey/test/MessageControllerTests.java @@ -14,10 +14,8 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; -import java.util.Calendar; import java.util.HashMap; import java.util.Map; -import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -27,20 +25,12 @@ import org.junit.Test; import com.mirth.connect.donkey.model.message.ConnectorMessage; -import com.mirth.connect.donkey.model.message.ContentType; -import com.mirth.connect.donkey.model.message.Message; -import com.mirth.connect.donkey.model.message.MessageContent; import com.mirth.connect.donkey.model.message.RawMessage; -import com.mirth.connect.donkey.model.message.Status; import com.mirth.connect.donkey.server.Donkey; import com.mirth.connect.donkey.server.StartException; import com.mirth.connect.donkey.server.channel.Channel; import com.mirth.connect.donkey.server.controllers.ChannelController; -import com.mirth.connect.donkey.server.controllers.MessageController; -import com.mirth.connect.donkey.server.data.DonkeyDao; import com.mirth.connect.donkey.server.data.timed.TimedDaoFactory; -import com.mirth.connect.donkey.server.queue.ConnectorMessageQueueDataSource; -import com.mirth.connect.donkey.test.util.TestChannel; import com.mirth.connect.donkey.test.util.TestUtils; import com.mirth.connect.donkey.util.ActionTimer; @@ -126,59 +116,6 @@ public void testCreateNewMessage() throws Exception { } } - @Test - public void testDeleteMessage() throws Exception { - TestChannel channel = TestUtils.createDefaultChannel(channelId, serverId); - channel.getSourceConnector().setRespondAfterProcessing(false); - channel.getSourceQueue().setDataSource(new ConnectorMessageQueueDataSource(channelId, serverId, 0, Status.RECEIVED, false, TestUtils.getDaoFactory())); - channel.getSourceQueue().updateSize(); - - Message message = null; - ConnectorMessage sourceMessage = null; - DonkeyDao dao = null; - - try { - dao = TestUtils.getDaoFactory().getDao(); - - message = new Message(); - message.setMessageId(dao.getNextMessageId(channelId)); - message.setChannelId(channelId); - message.setServerId(serverId); - message.setReceivedDate(Calendar.getInstance()); - - sourceMessage = new ConnectorMessage(channelId, channel.getName(), message.getMessageId(), 0, serverId, message.getReceivedDate(), Status.RECEIVED); - sourceMessage.setRaw(new MessageContent(channelId, message.getMessageId(), 0, ContentType.RAW, testMessage, null, false)); - message.getConnectorMessages().put(0, sourceMessage); - - dao.insertMessage(message); - dao.insertConnectorMessage(sourceMessage, true, true); - dao.insertMessageContent(sourceMessage.getRaw()); - dao.commit(); - } finally { - TestUtils.close(dao); - } - - // put the message in the source queue - channel.queue(sourceMessage); - - // assert that the message exists in the database - TestUtils.assertMessageExists(message, true); - TestUtils.assertConnectorMessageExists(sourceMessage, true); - - // assert that the message exists in the source queue's memory - assertTrue(channel.getSourceQueue().contains(sourceMessage)); - - // delete the message - Map> messages = new HashMap>(); - messages.put(message.getMessageId(), null); - MessageController.getInstance().deleteMessages(channelId, messages); - channel.invalidateQueues(); - - // assert that the message does not exist in the database - TestUtils.assertMessageDoesNotExist(message); - TestUtils.assertConnectorMessageDoesNotExist(sourceMessage); - - // assert that the message does not exist in the source queue's memory - assertTrue(!channel.getSourceQueue().contains(sourceMessage)); - } + // Deleting a message, and the queue that still holds it noticing, is covered against every + // dialect by the 240-message-deletion smoke tests. } diff --git a/donkey/src/test/java/com/mirth/connect/donkey/test/QueueTests.java b/donkey/src/test/java/com/mirth/connect/donkey/test/QueueTests.java deleted file mode 100644 index ccc259a182..0000000000 --- a/donkey/src/test/java/com/mirth/connect/donkey/test/QueueTests.java +++ /dev/null @@ -1,504 +0,0 @@ -/* - * Copyright (c) Mirth Corporation. All rights reserved. - * - * http://www.mirthcorp.com - * - * The software in this package is published under the terms of the MPL license a copy of which has - * been included with this distribution in the LICENSE.txt file. - */ - -package com.mirth.connect.donkey.test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; - -import com.mirth.connect.donkey.model.channel.DeployedState; -import com.mirth.connect.donkey.model.message.ConnectorMessage; -import com.mirth.connect.donkey.model.message.Message; -import com.mirth.connect.donkey.model.message.RawMessage; -import com.mirth.connect.donkey.model.message.Status; -import com.mirth.connect.donkey.server.Donkey; -import com.mirth.connect.donkey.server.StartException; -import com.mirth.connect.donkey.server.channel.ChannelException; -import com.mirth.connect.donkey.server.channel.DestinationChainProvider; -import com.mirth.connect.donkey.server.channel.DispatchResult; -import com.mirth.connect.donkey.server.controllers.ChannelController; -import com.mirth.connect.donkey.server.data.DonkeyDaoFactory; -import com.mirth.connect.donkey.server.queue.ConnectorMessageQueue; -import com.mirth.connect.donkey.server.queue.ConnectorMessageQueueDataSource; -import com.mirth.connect.donkey.server.queue.SourceQueue; -import com.mirth.connect.donkey.test.util.TestChannel; -import com.mirth.connect.donkey.test.util.TestDispatcher; -import com.mirth.connect.donkey.test.util.TestDispatcherProperties; -import com.mirth.connect.donkey.test.util.TestPostProcessor; -import com.mirth.connect.donkey.test.util.TestPreProcessor; -import com.mirth.connect.donkey.test.util.TestSourceConnector; -import com.mirth.connect.donkey.test.util.TestUtils; - -public class QueueTests { - private static int TEST_SIZE = 10; - private static String channelId = TestUtils.DEFAULT_CHANNEL_ID; - private static String channelName = TestUtils.DEFAULT_CHANNEL_ID; - private static String serverId = TestUtils.DEFAULT_SERVER_ID; - private static String testMessage = TestUtils.TEST_HL7_MESSAGE; - private static DonkeyDaoFactory daoFactory; - - @BeforeClass - final public static void beforeClass() throws StartException { - Donkey.getInstance().startEngine(TestUtils.getDonkeyTestConfiguration()); - daoFactory = TestUtils.getDaoFactory(); - } - - @AfterClass - final public static void afterClass() throws StartException { - Donkey.getInstance().stopEngine(); - } - - /* - * Create a new DatabaseLinkedBlockingQueue Set the select and count statements, fill the queue - * buffer, and assert that: - The buffer capacity was set correctly - The buffer size is correct - * - * Send messages, then assert that: - All messages were successfully put in the queue - The - * buffer size is still correct - * - * Flush the queue, then assert that: - The queue size shrank correctly - The buffer size shrank - * correctly - */ - @Test - public final void testDatabaseLinkedBlockingQueue() throws SQLException { - int testSize = TEST_SIZE; - int bufferCapacity = 10; - - TestUtils.initChannel(channelId); - - SourceQueue queue = new SourceQueue(); - queue.setBufferCapacity(bufferCapacity); - queue.setDataSource(new ConnectorMessageQueueDataSource(channelId, serverId, 0, Status.RECEIVED, false, daoFactory)); - queue.updateSize(); - queue.fillBuffer(); - - assertEquals(bufferCapacity, queue.getBufferCapacity()); - assertEquals(Math.min(bufferCapacity, queue.size()), queue.getBufferSize()); - - int initialSize = queue.size(); - int initialBufferSize = queue.getBufferSize(); - - for (int i = 0; i < testSize; i++) { - ConnectorMessage connectorMessage = TestUtils.createAndStoreNewMessage(new RawMessage(testMessage), channelId, channelName, serverId, daoFactory).getConnectorMessages().get(0); - queue.add(connectorMessage); - } - - assertEquals(initialSize + testSize, queue.size()); - assertEquals(Math.min(bufferCapacity, queue.size()), queue.getBufferSize()); - - for (int i = 0; i < testSize; i++) { - try { - queue.poll(10, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - assertEquals(initialSize, queue.size()); - assertEquals(Math.max(0, initialBufferSize - testSize), queue.getBufferSize()); - } - - /* - * Start up a new channel, and assert that: - The channel's queue thread is currently running - * Then stop the channel and assert that: - The channel's queue thread has stopped running - * - * While the channel is stopped, send messages, assert that: - The queue size is equal to the - * test size Start the channel back up, wait a while, assert that: - The queue size is zero - * - * While the channel is still running, send messages, assert that: - The queue size is greater - * than zero Wait a while, then assert that: - The queue size is zero - */ - @Test - public final void testSourceQueue() throws Exception { - TestChannel channel = (TestChannel) TestUtils.createDefaultChannel(channelId, serverId); - channel.getSourceConnector().setRespondAfterProcessing(false); - - // Start up a default channel - channel.deploy(); - channel.start(null); - - // give the queue thread time to start - Thread.sleep(3000); - - // Assert that the source queue thread is running - assertTrue(channel.isQueueThreadRunning()); - - // Stop the channel - channel.stop(); - - // Assert that the source queue thread is not longer running - assertFalse(channel.isQueueThreadRunning()); - - // Place messages in the channel's source queue while the channel is stopped - for (int i = 1; i <= TEST_SIZE; i++) { - ConnectorMessage sourceMessage = TestUtils.createAndStoreNewMessage(new RawMessage(testMessage), channel.getChannelId(), channel.getName(), channel.getServerId(), daoFactory).getConnectorMessages().get(0); - channel.queue(sourceMessage); - } - - // Assert that all the messages were successfully placed in the queue - assertEquals(channel.getSourceQueue().size(), TEST_SIZE); - - // Start up the channel - channel.start(null); - - Thread.sleep(500 * TEST_SIZE); - - // Assert that the queue has been cleared - assertEquals(channel.getSourceQueue().size(), 0); - - // Place messages in the channel's source queue while the channel is started - for (int i = 1; i <= TEST_SIZE; i++) { - ConnectorMessage sourceMessage = TestUtils.createAndStoreNewMessage(new RawMessage(testMessage), channel.getChannelId(), channel.getName(), channel.getServerId(), daoFactory).getConnectorMessages().get(0); - channel.queue(sourceMessage); - } - - // Assert that the queue size is greater than zero (may fail for small test sizes) - assertTrue(channel.getSourceQueue().size() > 0); - - Thread.sleep(500 * TEST_SIZE); - - // Assert that the queue has cleared - assertEquals(channel.getSourceQueue().size(), 0); - - channel.stop(); - channel.undeploy(); - ChannelController.getInstance().removeChannel(channelId); - } - - /* - * Start up a new channel, queue up more messages in the channel than the size of the source - * queue buffer - * - * Send a message (not waiting for destinations) that will cause the data to get written to the - * database, but the queue size is never incremented - * - * Asynchronously queue up another message normally, wait a bit to let the asynchronous message - * get written to the database and queued - * - * Queue up the message from before that wasn't queued, wait for the queue to clear, then assert - * that: - The number of messages processed through the channel is less than the initial number - * of messages sent plus two - The second-to-last message in the messageIds list was processed, - * but the last one was not - */ - @Test - public final void testSourceQueueOrderAsync() throws Exception { - int testSize = 2; - final List messageIds = new ArrayList(); - final TestChannel channel = (TestChannel) TestUtils.createDefaultChannel(channelId, serverId); - channel.getSourceConnector().setRespondAfterProcessing(false); - - // Deploy a default channel - channel.deploy(); - channel.start(null); - - ConnectorMessageQueue sourceQueue = channel.getSourceQueue(); - int initialSize = sourceQueue.getBufferCapacity() * testSize + 1; - - // Queue up messages normally - System.out.println("Queuing " + initialSize + " messages normally..."); - for (int i = 1; i <= initialSize; i++) { - DispatchResult dispatchResult = channel.getSourceConnector().dispatchRawMessage(new RawMessage(testMessage, null, null)); - messageIds.add(dispatchResult.getMessageId()); - channel.getSourceConnector().finishDispatch(dispatchResult); - } - - ConnectorMessage sourceMessage = null; - - /* - * Send a message (not waiting for destinations) that will cause the data to get written to - * the database, but the queue size is never incremented - */ - System.out.println("Saving a message to the database without calling the queue method..."); - RawMessage rawMessage = new RawMessage(testMessage, null, null); - sourceMessage = TestUtils.createAndStoreNewMessage(rawMessage, channel.getChannelId(), channel.getName(), channel.getServerId(), daoFactory).getConnectorMessages().get(0); - messageIds.add(sourceMessage.getMessageId()); - - // Asynchronously queue up another message normally - Thread thread = new Thread() { - @Override - public void run() { - System.out.println("Queuing another message normally and asynchronously..."); - DispatchResult dispatchResult = null; - - try { - dispatchResult = channel.getSourceConnector().dispatchRawMessage(new RawMessage(testMessage, null, null)); - } catch (ChannelException e) { - throw new AssertionError(e); - } finally { - channel.getSourceConnector().finishDispatch(dispatchResult); - } - - messageIds.add(dispatchResult.getMessageId()); - } - }; - thread.start(); - - /* - * Wait until the queue has cleared to simulate a delay between committing the message to - * the database and adding it to the channel's queue. Meanwhile the asynchronous message - * should be queued - */ - while (sourceQueue.size() > 0 && channel.isQueueThreadRunning() && channel.getCurrentState() == DeployedState.STARTED) { - System.out.println("Waiting for queue to clear, size: " + sourceQueue.size()); - Thread.sleep(1000); - } - - /* - * Queue up the message from before that wasn't queued. Even though this message has the - * lower message ID, because the asynchronous message was queued before this one (increasing - * the queue size), the queue buffer will have been filled with THIS message (not the - * asynchronous one). Therefore, this message should have already been processed through the - * channel. Queuing the same message again should cause a foreign key constraint violation. - */ - System.out.println("Calling the queue method for the previous message that wasn't queued..."); - channel.queue(sourceMessage); - - // Wait until the queue has cleared - while (sourceQueue.size() > 0 && channel.isQueueThreadRunning() && channel.getCurrentState() == DeployedState.STARTED) { - System.out.println("Waiting for queue to clear, size: " + sourceQueue.size()); - Thread.sleep(5000); - } - - // Assert that the number of messages processed through the channel is NOT correct - assertTrue(channel.getNumMessages() < initialSize + 2); - - // Assert that the second-to-last message in the messageIds list was processed, but the last one was not - assertTrue(channel.getMessageIds().contains(messageIds.get(messageIds.size() - 2))); - assertFalse(channel.getMessageIds().contains(messageIds.get(messageIds.size() - 1))); - - channel.stop(); - channel.undeploy(); - - ChannelController.getInstance().removeChannel(channel.getChannelId()); - } - - /* - * Start up a new channel, queue up more messages in the channel than the size of the source - * queue buffer - * - * Synchronize on the channel's source queue so that commits and queue additions happen in the - * same thread always - * - * Send a message (not waiting for destinations) that will cause the data to get written to the - * database, but the queue size is never incremented - * - * Asynchronously queue up another message normally, wait a while to simulate a delay between - * the commit and queue addition (the asynchronous message should not be queued) - * - * Queue up the message from before that wasn't queued, wait for the queue to clear, then assert - * that: - The number of messages processed through the channel is equal to the initial number - * of messages sent plus two - The messageIds list is identical (size and order) to the message - * ID list generated by the test channel (that is, all messages successfully processed through - * the channel and in the right order) - */ - @Test - public final void testSourceQueueOrderSync() throws Exception { - int testSize = 2; - final List messageIds = new ArrayList(); - final TestChannel channel = (TestChannel) TestUtils.createDefaultChannel(channelId, serverId); - channel.getSourceConnector().setRespondAfterProcessing(false); - - // Deploy a default channel - channel.deploy(); - channel.start(null); - - ConnectorMessageQueue sourceQueue = channel.getSourceQueue(); - int initialSize = sourceQueue.getBufferCapacity() * testSize + 1; - - // Queue up messages normally - System.out.println("Queuing " + initialSize + " messages normally..."); - for (int i = 1; i <= initialSize; i++) { - DispatchResult dispatchResult = channel.getSourceConnector().dispatchRawMessage(new RawMessage(testMessage, null, null)); - messageIds.add(dispatchResult.getMessageId()); - channel.getSourceConnector().finishDispatch(dispatchResult); - } - - ConnectorMessage sourceMessage = null; - - synchronized (channel.getSourceQueue()) { - /* - * Send a message (not waiting for destinations) that will cause the data to get written - * to the database, but the queue size is never incremented - */ - System.out.println("Saving a message to the database without calling the queue method..."); - RawMessage rawMessage = new RawMessage(testMessage, null, null); - sourceMessage = TestUtils.createAndStoreNewMessage(rawMessage, channel.getChannelId(), channel.getName(), channel.getServerId(), daoFactory).getConnectorMessages().get(0); - messageIds.add(sourceMessage.getMessageId()); - - // Asynchronously queue up another message normally - Thread thread = new Thread() { - @Override - public void run() { - System.out.println("Queuing another message normally and asynchronously..."); - DispatchResult dispatchResult = null; - - try { - dispatchResult = channel.getSourceConnector().dispatchRawMessage(new RawMessage(testMessage, null, null)); - } catch (ChannelException e) { - throw new AssertionError(e); - } finally { - channel.getSourceConnector().finishDispatch(dispatchResult); - } - - messageIds.add(dispatchResult.getMessageId()); - } - }; - thread.start(); - - /* - * Wait a while to simulate a delay between committing the message to the database and - * adding it to the channel's queue. The thread processing the asynchronous message - * should be waiting on this block to finish, so it should not add the message to the - * source queue - */ - System.out.println("Waiting ten seconds..."); - Thread.sleep(10000); - - // Queue up the message from before that wasn't queued - System.out.println("Calling the queue method for the previous message that wasn't queued..."); - channel.queue(sourceMessage); - } - - // Wait until the queue has cleared - while (sourceQueue.size() > 0 && channel.isQueueThreadRunning() && channel.getCurrentState() == DeployedState.STARTED) { - System.out.println("Waiting for queue to clear, size: " + sourceQueue.size()); - Thread.sleep(5000); - } - - // Assert that the number of messages processed through the channel is correct - assertEquals(initialSize + 2, channel.getNumMessages()); - // Assert that all messages were processed through the channel in the order of database insertion - assertTrue(messageIds.equals(channel.getMessageIds())); - - channel.stop(); - channel.undeploy(); - - ChannelController.getInstance().removeChannel(channel.getChannelId()); - } - - /* - * Create a new channel with a test dispatcher destination connector The dispatcher initially - * queues all messages Start up the channel, and assert that: - The destination connector queue - * thread is running - * - * Send messages, and assert that: - The queue size is equal to the test size - * - * Change the response status that the dispatcher returns to SENT Wait a bit, then assert that: - * - The queue size is zero - * - * Stop the channel, assert that: - The destination connector queue thread is not running - * - * Place messages directly in the destination connector queue, assert that: - All the messages - * were successfully put in the queue - * - * Start the channel, wait a bit, then assert that: - The queue size is zero - */ - @Test - public final void testDestinationQueue() throws Exception { - TestUtils.initChannel(channelId); - - TestChannel channel = new TestChannel(); - - channel.setChannelId(channelId); - channel.setServerId(serverId); - - channel.setPreProcessor(new TestPreProcessor()); - channel.setPostProcessor(new TestPostProcessor()); - - TestSourceConnector sourceConnector = (TestSourceConnector) TestUtils.createDefaultSourceConnector(); - sourceConnector.setChannelId(channel.getChannelId()); - sourceConnector.setChannel(channel); - channel.setSourceConnector(sourceConnector); - channel.getSourceConnector().setFilterTransformerExecutor(TestUtils.createDefaultFilterTransformerExecutor()); - - // The TestDispatcher send method initially always returns a response of QUEUED - TestDispatcher destinationConnector = new TestDispatcher(); - - TestDispatcherProperties connectorProperties = new TestDispatcherProperties(); - connectorProperties.setTemplate(testMessage); - connectorProperties.getDestinationConnectorProperties().setQueueEnabled(true); - connectorProperties.getDestinationConnectorProperties().setRegenerateTemplate(true); - - TestUtils.initDefaultDestinationConnector(destinationConnector, connectorProperties); - destinationConnector.setChannelId(channelId); - - destinationConnector.setMetaDataReplacer(sourceConnector.getMetaDataReplacer()); - destinationConnector.setMetaDataColumns(channel.getMetaDataColumns()); - destinationConnector.setFilterTransformerExecutor(TestUtils.createDefaultFilterTransformerExecutor()); - - DestinationChainProvider chain = new DestinationChainProvider(); - chain.setChannelId(channelId); - chain.addDestination(1, destinationConnector); - channel.addDestinationChainProvider(chain); - - // Start up the channel - channel.deploy(); - channel.start(null); - - Thread.sleep(1000); - - // Assert that the destination connector queue thread is running - assertTrue(destinationConnector.isQueueThreadRunning()); - - // Send messages while the channel is started - for (int i = 1; i <= TEST_SIZE; i++) { - sourceConnector.readTestMessage(testMessage); - } - - // Since the messages should all get queued, assert that the queue size is equal to the test size - assertEquals(destinationConnector.getQueue().size(), TEST_SIZE); - - // Tell the dispatcher to now send a response status of SENT - destinationConnector.setReturnStatus(Status.SENT); - - Thread.sleep(500 * TEST_SIZE); - - // Assert that all the queued messages have been sent - assertEquals(destinationConnector.getQueue().size(), 0); - - // Stop the channel - channel.stop(); - - // Assert that the destination connector queue thread is not running - assertFalse(destinationConnector.isQueueThreadRunning()); - - // Place messages directly into the destination connector's queue - for (int i = 1; i <= TEST_SIZE; i++) { - synchronized (destinationConnector.getQueue()) { - Message message = TestUtils.createAndStoreNewMessage(new RawMessage(testMessage), channelId, channelName, serverId, daoFactory); - ConnectorMessage destinationMessage = TestUtils.createAndStoreDestinationConnectorMessage(daoFactory, channelId, channelName, serverId, message.getMessageId(), destinationConnector.getMetaDataId(), testMessage, Status.QUEUED); - destinationConnector.getQueue().add(destinationMessage); - } - } - - // Assert that all the messages were successfully placed in the queue - assertEquals(TEST_SIZE, destinationConnector.getQueue().size() - 1); - - // Start the channel back up - channel.start(null); - - Thread.sleep(500 * TEST_SIZE); - - // Since the dispatcher should still always be returning a response status of SENT, the queue should be clear again - assertEquals(0, destinationConnector.getQueue().size()); - - channel.stop(); - channel.undeploy(); - ChannelController.getInstance().removeChannel(channelId); - } -} diff --git a/donkey/src/test/java/com/mirth/connect/donkey/test/SourceConnectorTests.java b/donkey/src/test/java/com/mirth/connect/donkey/test/SourceConnectorTests.java index c4c8560167..1d414afe86 100644 --- a/donkey/src/test/java/com/mirth/connect/donkey/test/SourceConnectorTests.java +++ b/donkey/src/test/java/com/mirth/connect/donkey/test/SourceConnectorTests.java @@ -21,7 +21,6 @@ import com.mirth.connect.donkey.model.channel.SourceConnectorProperties; import com.mirth.connect.donkey.model.message.Message; import com.mirth.connect.donkey.model.message.RawMessage; -import com.mirth.connect.donkey.model.message.Response; import com.mirth.connect.donkey.model.message.Status; import com.mirth.connect.donkey.server.Donkey; import com.mirth.connect.donkey.server.StartException; @@ -157,134 +156,7 @@ public final void testHandleRawMessage() throws Exception { } /* - * Deploy a new channel, send messages, assert that: - The message was stored in the database - - * The MessageResponse returned is not null - The response is null - The source connector - * response was not stored - * - * Call storeMessageResponse using a null MessageResponse assert: - The source connector - * response was not stored - * - * Call storeMessageResponse using the returned MessageResponse, assert: - The source connector - * response was not stored - * - * Modify the MessageResponse, creating/setting a new Response object Call storeMessageResponse - * using the returned MessageResponse, assert: - The source connector response was stored - */ - @Test - public final void testStoreMessageResponse() throws Exception { - TestChannel channel = (TestChannel) TestUtils.createDefaultChannel(channelId, serverId); - - TestSourceConnector sourceConnector = (TestSourceConnector) channel.getSourceConnector(); - sourceConnector.setRespondAfterProcessing(true); - channel.getResponseSelector().setRespondFromName(SourceConnectorProperties.RESPONSE_SOURCE_TRANSFORMED); - - channel.deploy(); - channel.start(null); - - for (int i = 1; i <= TEST_SIZE; i++) { - RawMessage rawMessage = new RawMessage(testMessage); - DispatchResult dispatchResult = null; - - try { - dispatchResult = sourceConnector.dispatchRawMessage(rawMessage); - dispatchResult.setAttemptedResponse(true); - - if (dispatchResult.getSelectedResponse() != null) { - dispatchResult.getSelectedResponse().setMessage("response"); - } - } finally { - sourceConnector.finishDispatch(dispatchResult); - } - - if (dispatchResult != null) { - sourceConnector.getMessageIds().add(dispatchResult.getMessageId()); - } - - // Assert that the message was created - Message message = new Message(); - message.setChannelId(channel.getChannelId()); - message.setMessageId(dispatchResult.getMessageId()); - message.setServerId(channel.getServerId()); - message.setProcessed(true); - TestUtils.assertMessageExists(message, false); - - // Assert that the message response is not null - assertNotNull(dispatchResult); - - // Assert that the response is not null - assertNotNull(dispatchResult.getSelectedResponse()); - - // Assert that the source connector response was created - TestUtils.assertResponseExists(channel.getChannelId(), dispatchResult.getMessageId()); - } - - channel.stop(); - channel.undeploy(); - - ChannelController.getInstance().removeChannel(channel.getChannelId()); - } - - /* - * Deploys and starts a channel, sets the source respond-from name to - * RESPONSE_SOURCE_TRANSFORMED, sends messages, and asserts that: - Each response returned is - * TRANSFORMED - TEST_SIZE messages are processed - * - * Then sets the source respond-from name to RESPONSE_DESTINATIONS_COMPLETED, sends messages, - * and asserts that: - Each response returned is SENT - 2*TEST_SIZE messages are processed - * - * Then sets the source respond-from name to the destination name, sends messages, and asserts - * that: - Each response returned is SENT - 3*TEST_SIZE messages are processed - * - * Then sets the source respond-from name to an invalid destination name, sends messages, and - * asserts that: - Each response returned is null - 4*TEST_SIZE messages are processed + * testStoreMessageResponse and testGetResponse are re-implemented as CI fixtures in + * ci/tests/190-response-handling. */ - @Test - public final void testGetResponse() throws Exception { - String destinationName = TestUtils.DEFAULT_DESTINATION_NAME; - TestChannel channel = (TestChannel) TestUtils.createDefaultChannel(channelId, serverId); - TestSourceConnector sourceConnector = (TestSourceConnector) channel.getSourceConnector(); - Response response = null; - - channel.deploy(); - channel.start(null); - - channel.getResponseSelector().setRespondFromName(SourceConnectorProperties.RESPONSE_SOURCE_TRANSFORMED); - - for (int i = 0; i < TEST_SIZE; i++) { - response = sourceConnector.readTestMessage(testMessage).getSelectedResponse(); - assertEquals(Status.TRANSFORMED, response.getStatus()); - } - - assertEquals(TEST_SIZE, channel.getNumMessages()); - - channel.getResponseSelector().setRespondFromName(SourceConnectorProperties.RESPONSE_DESTINATIONS_COMPLETED); - - for (int i = 0; i < TEST_SIZE; i++) { - response = sourceConnector.readTestMessage(testMessage).getSelectedResponse(); - assertEquals(Status.SENT, response.getStatus()); - } - - assertEquals(TEST_SIZE * 2, channel.getNumMessages()); - - channel.getResponseSelector().setRespondFromName("d1"); - - for (int i = 0; i < TEST_SIZE; i++) { - response = sourceConnector.readTestMessage(testMessage).getSelectedResponse(); - assertEquals(Status.SENT, response.getStatus()); - } - - assertEquals(TEST_SIZE * 3, channel.getNumMessages()); - - channel.getResponseSelector().setRespondFromName(destinationName + "lolwut"); - - for (int i = 0; i < TEST_SIZE; i++) { - response = sourceConnector.readTestMessage(testMessage).getSelectedResponse(); - assertEquals(null, response); - } - - assertEquals(TEST_SIZE * 4, channel.getNumMessages()); - - channel.stop(); - channel.undeploy(); - } } \ No newline at end of file diff --git a/donkey/src/test/java/com/mirth/connect/donkey/test/StatisticsTests.java b/donkey/src/test/java/com/mirth/connect/donkey/test/StatisticsTests.java deleted file mode 100644 index 10a93925d8..0000000000 --- a/donkey/src/test/java/com/mirth/connect/donkey/test/StatisticsTests.java +++ /dev/null @@ -1,569 +0,0 @@ -/* - * Copyright (c) Mirth Corporation. All rights reserved. - * - * http://www.mirthcorp.com - * - * The software in this package is published under the terms of the MPL license a copy of which has - * been included with this distribution in the LICENSE.txt file. - */ - -package com.mirth.connect.donkey.test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import java.util.HashMap; -import java.util.Map; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; - -import com.mirth.connect.donkey.model.DonkeyException; -import com.mirth.connect.donkey.model.channel.ConnectorProperties; -import com.mirth.connect.donkey.model.message.ConnectorMessage; -import com.mirth.connect.donkey.model.message.Response; -import com.mirth.connect.donkey.model.message.Status; -import com.mirth.connect.donkey.server.Donkey; -import com.mirth.connect.donkey.server.StartException; -import com.mirth.connect.donkey.server.channel.DestinationChainProvider; -import com.mirth.connect.donkey.server.channel.DestinationConnector; -import com.mirth.connect.donkey.server.channel.FilterTransformerExecutor; -import com.mirth.connect.donkey.server.channel.FilterTransformerResult; -import com.mirth.connect.donkey.server.channel.components.FilterTransformerException; -import com.mirth.connect.donkey.server.controllers.ChannelController; -import com.mirth.connect.donkey.server.data.DonkeyDaoFactory; -import com.mirth.connect.donkey.server.queue.ConnectorMessageQueueDataSource; -import com.mirth.connect.donkey.server.queue.DestinationQueue; -import com.mirth.connect.donkey.test.util.TestChannel; -import com.mirth.connect.donkey.test.util.TestDataType; -import com.mirth.connect.donkey.test.util.TestDispatcher; -import com.mirth.connect.donkey.test.util.TestDispatcherProperties; -import com.mirth.connect.donkey.test.util.TestFilterTransformer; -import com.mirth.connect.donkey.test.util.TestPostProcessor; -import com.mirth.connect.donkey.test.util.TestPreProcessor; -import com.mirth.connect.donkey.test.util.TestResponseTransformer; -import com.mirth.connect.donkey.test.util.TestSourceConnector; -import com.mirth.connect.donkey.test.util.TestUtils; - -public class StatisticsTests { - private static long TEST_SIZE = 100; - private static String channelId = TestUtils.DEFAULT_CHANNEL_ID; - private static String serverId = TestUtils.DEFAULT_SERVER_ID; - private static String testMessage = TestUtils.TEST_HL7_MESSAGE; - private static DonkeyDaoFactory daoFactory; - private Logger logger = LogManager.getLogger(this.getClass()); - - @BeforeClass - final public static void beforeClass() throws StartException { - Donkey.getInstance().startEngine(TestUtils.getDonkeyTestConfiguration()); - daoFactory = TestUtils.getDaoFactory(); - } - - @AfterClass - final public static void afterClass() throws StartException { - Donkey.getInstance().stopEngine(); - } - - /* - * Create a "normal" channel with four destination connectors where all messages process through - * the source connector with a final status of RECEIVED, and process through each destination - * connector with a status of SENT - * - * Send messages, assert that: - The source connector stats have TEST_SIZE for RECEIVED and - * TRANSFORMED - The destination connector stats have TEST_SIZE for RECEIVED and SENT - The - * aggregate stats are correct (RECEIVED/TRANSFORMED is the same as the source connector, - * PENDING/SENT/QUEUED is the same as the destination connectors combined, FILTERED/ERROR is the - * same as all connectors combined) - */ - @Test - public final void testStatistics1() throws Exception { - TestChannel channel = (TestChannel) TestUtils.createDefaultChannel(channelId, serverId, true, 2, 2); - - channel.deploy(); - channel.start(null); - - // Send messages through the channel - for (int i = 1; i <= TEST_SIZE; i++) { - ((TestSourceConnector) channel.getSourceConnector()).readTestMessage(testMessage); - } - - // Assert that the messages were sent - assertEquals(TEST_SIZE, channel.getNumMessages()); - - // Assert the source connector stats are correct - - statsEqual(channel.getChannelId(), 0, TEST_SIZE, 0l, TEST_SIZE, 0l, 0l, 0l); - // Assert that destination connector stats are correct - statsEqual(channel.getChannelId(), 1, TEST_SIZE, 0l, 0l, 0l, TEST_SIZE, 0l); - statsEqual(channel.getChannelId(), 2, TEST_SIZE, 0l, 0l, 0l, TEST_SIZE, 0l); - statsEqual(channel.getChannelId(), 3, TEST_SIZE, 0l, 0l, 0l, TEST_SIZE, 0l); - statsEqual(channel.getChannelId(), 4, TEST_SIZE, 0l, 0l, 0l, TEST_SIZE, 0l); - // Assert the aggregate stats are correct - assertTrue(channelStatsCorrect()); - - channel.stop(); - channel.undeploy(); - - ChannelController.getInstance().removeChannel(channel.getChannelId()); - } - - /* - * Create a channel where all messages are filtered on the source connector - * - * Send messages, assert that: - The source connector stats have TEST_SIZE for RECEIVED and - * FILTERED - The destination connector stats are all zeroes - The aggregate stats are correct - * (RECEIVED/TRANSFORMED is the same as the source connector, PENDING/SENT/QUEUED is the same as - * the destination connectors combined, FILTERED/ERROR is the same as all connectors combined) - */ - @Test - public final void testStatistics2() throws Exception { - TestChannel channel = (TestChannel) TestUtils.createDefaultChannel(channelId, serverId, true, 2, 2); - - TestFilterTransformer filterTransformer = new TestFilterTransformer() { - @Override - public FilterTransformerResult doFilterTransform(ConnectorMessage message) throws FilterTransformerException { - super.doFilterTransform(message); - return new FilterTransformerResult(true, null); - } - }; - channel.getSourceConnector().getFilterTransformerExecutor().setFilterTransformer(filterTransformer); - - channel.deploy(); - channel.start(null); - - // Send messages through the channel - for (int i = 1; i <= TEST_SIZE; i++) { - ((TestSourceConnector) channel.getSourceConnector()).readTestMessage(testMessage); - } - - // Assert that the messages were sent - assertEquals(TEST_SIZE, channel.getNumMessages()); - - // Assert the source connector stats are correct - statsEqual(channel.getChannelId(), 0, TEST_SIZE, TEST_SIZE, 0L, 0L, 0L, 0L); - // Assert that destination connector stats are correct - statsEqual(channel.getChannelId(), 1, 0L, 0L, 0L, 0L, 0L, 0L); - statsEqual(channel.getChannelId(), 2, 0L, 0L, 0L, 0L, 0L, 0L); - statsEqual(channel.getChannelId(), 3, 0L, 0L, 0L, 0L, 0L, 0L); - statsEqual(channel.getChannelId(), 4, 0L, 0L, 0L, 0L, 0L, 0L); - // Assert the aggregate stats are correct - assertTrue(channelStatsCorrect()); - - channel.stop(); - channel.undeploy(); - - ChannelController.getInstance().removeChannel(channel.getChannelId()); - } - - /* - * Create a channel where each destination results in a different status (FILTERED, SENT, - * QUEUED, ERROR) - * - * Send messages, assert that: - The source connector stats have TEST_SIZE for RECEIVED and - * TRANSFORMED - The first destination connector stats have TEST_SIZE for FILTERED - The second - * destination connector stats have TEST_SIZE for SENT - The third destination connector stats - * have TEST_SIZE for QUEUED - The fourth destination connector stats have TEST_SIZE for ERROR - - * The aggregate stats are correct (RECEIVED/TRANSFORMED is the same as the source connector, - * PENDING/SENT/QUEUED is the same as the destination connectors combined, FILTERED/ERROR is the - * same as all connectors combined) - */ - @Test - public final void testStatistics3() throws Exception { - TestUtils.initChannel(channelId); - - TestChannel channel = new TestChannel(); - - channel.setChannelId(channelId); - channel.setServerId(serverId); - - channel.setPreProcessor(new TestPreProcessor()); - channel.setPostProcessor(new TestPostProcessor()); - - TestSourceConnector sourceConnector = (TestSourceConnector) TestUtils.createDefaultSourceConnector(); - sourceConnector.setChannelId(channel.getChannelId()); - sourceConnector.setChannel(channel); - channel.setSourceConnector(sourceConnector); - channel.getSourceConnector().setFilterTransformerExecutor(TestUtils.createDefaultFilterTransformerExecutor()); - - DestinationChainProvider chain = new DestinationChainProvider(); - chain.setChannelId(channel.getChannelId()); - - for (int i = 1; i <= 4; i++) { - DestinationConnector destinationConnector = new TestDispatcher(); - destinationConnector.setChannelId(channel.getChannelId()); - - TestDispatcherProperties connectorProperties = new TestDispatcherProperties(); - if (i == 3) { - connectorProperties.getDestinationConnectorProperties().setQueueEnabled(true); - connectorProperties.getDestinationConnectorProperties().setSendFirst(false); - } else { - connectorProperties.getDestinationConnectorProperties().setQueueEnabled(false); - } - - destinationConnector.setConnectorProperties(connectorProperties); - destinationConnector.setDestinationName(TestUtils.DEFAULT_DESTINATION_NAME); - destinationConnector.setInboundDataType(new TestDataType()); - destinationConnector.setOutboundDataType(new TestDataType()); - destinationConnector.setResponseTransformerExecutor(TestUtils.createDefaultResponseTransformerExecutor()); - - DestinationQueue destinationConnectorQueue = new DestinationQueue(connectorProperties.getDestinationConnectorProperties().getThreadAssignmentVariable(), connectorProperties.getDestinationConnectorProperties().getThreadCount(), connectorProperties.getDestinationConnectorProperties().isRegenerateTemplate(), destinationConnector.getSerializer(), destinationConnector.getMessageMaps()); - destinationConnectorQueue.setDataSource(new ConnectorMessageQueueDataSource(channel.getChannelId(), channel.getServerId(), i, Status.QUEUED, false, daoFactory)); - destinationConnectorQueue.updateSize(); - destinationConnector.setQueue(destinationConnectorQueue); - - FilterTransformerExecutor filterTransformerExecutor = TestUtils.createDefaultFilterTransformerExecutor(); - - switch (i) { - case 1: - ((TestFilterTransformer) filterTransformerExecutor.getFilterTransformer()).setFiltered(true); - break; - case 2: - ((TestDispatcher) destinationConnector).setReturnStatus(Status.SENT); - break; - case 3: - ((TestDispatcher) destinationConnector).setReturnStatus(Status.QUEUED); - break; - case 4: - ((TestDispatcher) destinationConnector).setReturnStatus(Status.ERROR); - break; - } - - destinationConnector.setMetaDataReplacer(sourceConnector.getMetaDataReplacer()); - destinationConnector.setMetaDataColumns(channel.getMetaDataColumns()); - destinationConnector.setFilterTransformerExecutor(filterTransformerExecutor); - - chain.addDestination(i, destinationConnector); - } - - channel.addDestinationChainProvider(chain); - - ChannelController.getInstance().deleteAllMessages(channel.getChannelId()); - TestUtils.deleteChannelStatistics(channel.getChannelId()); - channel.deploy(); - channel.start(null); - - // Send messages through the channel - for (int i = 1; i <= TEST_SIZE; i++) { - ((TestSourceConnector) channel.getSourceConnector()).readTestMessage(testMessage); - } - - // Assert that the messages were sent - assertEquals(TEST_SIZE, channel.getNumMessages()); - - // Assert the source connector stats are correct - statsEqual(channel.getChannelId(), 0, TEST_SIZE, 0L, TEST_SIZE, 0L, 0L, 0L); - // Assert that destination connector stats are correct - statsEqual(channel.getChannelId(), 1, TEST_SIZE, TEST_SIZE, 0L, 0L, 0L, 0L); - statsEqual(channel.getChannelId(), 2, TEST_SIZE, 0L, 0L, 0L, TEST_SIZE, 0L); - statsEqual(channel.getChannelId(), 3, TEST_SIZE, 0L, 0L, 0L, 0L, 0L); - statsEqual(channel.getChannelId(), 4, TEST_SIZE, 0L, 0L, 0L, 0L, TEST_SIZE); - // Assert the aggregate stats are correct - assertTrue(channelStatsCorrect()); - - channel.stop(); - channel.undeploy(); - - ChannelController.getInstance().removeChannel(channel.getChannelId()); - } - - /* - * Create a channel where the destination connector sends messages as usual, but both the - * dispatcher's send method and the response transformer's doTransform method stall the - * processing thread for a specified amount of time. This way, we can check at specific - * intervals whether the message statistics in the database have been updated to QUEUED, - * PENDING, and the final return status. - * - * Send messages, and for each message: Wait a specified amount of time, then assert: - The - * destination connector stats have 1 for QUEUED, the total number of messages for RECEIVED, and - * the remainder for the final return status. - * - * Wait a specified amount of time, then assert: - The destination connector stats have 1 for - * PENDING, the total number of messages for RECEIVED, and the remainder for the final return - * status. - * - * Wait a specified amount of time, then assert: - The source connector stats have the total - * number of messages for RECEIVED and TRANSFORMED - The destination connector stats have the - * total number of messages for RECEIVED and the final return status. - The aggregate stats are - * correct - * - * After all messages finish processing, assert: - The source connector stats have testSize for - * RECEIVED and TRANSFORMED - The destination connector stats have testSize for RECEIVED and the - * final return status - The aggregate stats are correct (RECEIVED/TRANSFORMED is the same as - * the source connector, PENDING/SENT/QUEUED is the same as the destination connectors combined, - * FILTERED/ERROR is the same as all connectors combined) - */ - @Test - public final void testStatistics4() throws Exception { - final int waitTime = 1000; - long testSize = 5; - Status returnStatus = Status.SENT; - - TestUtils.initChannel(channelId); - - TestChannel channel = new TestChannel(); - - channel.setChannelId(channelId); - channel.setServerId(serverId); - - channel.setPreProcessor(new TestPreProcessor()); - channel.setPostProcessor(new TestPostProcessor()); - - TestSourceConnector sourceConnector = (TestSourceConnector) TestUtils.createDefaultSourceConnector(); - sourceConnector.setChannelId(channel.getChannelId()); - sourceConnector.setChannel(channel); - channel.setSourceConnector(sourceConnector); - channel.getSourceConnector().setFilterTransformerExecutor(TestUtils.createDefaultFilterTransformerExecutor()); - - DestinationChainProvider chain = new DestinationChainProvider(); - chain.setChannelId(channel.getChannelId()); - - class BlockingTestDispatcher extends TestDispatcher { - public volatile boolean waiting = true; - - @Override - public Response send(ConnectorProperties connectorProperties, ConnectorMessage message) { - while (waiting) { - try { - Thread.sleep(waitTime); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - return super.send(connectorProperties, message); - } - } - - // Create a destination connector that stalls the processing thread a specified amount of time during the send method - BlockingTestDispatcher destinationConnector = new BlockingTestDispatcher(); - destinationConnector.setChannelId(channel.getChannelId()); - - destinationConnector.setMetaDataReplacer(sourceConnector.getMetaDataReplacer()); - destinationConnector.setMetaDataColumns(channel.getMetaDataColumns()); - destinationConnector.setFilterTransformerExecutor(TestUtils.createDefaultFilterTransformerExecutor()); - - TestDispatcherProperties connectorProperties = new TestDispatcherProperties(); - connectorProperties.getDestinationConnectorProperties().setQueueEnabled(true); - connectorProperties.getDestinationConnectorProperties().setSendFirst(false); - - destinationConnector.setConnectorProperties(connectorProperties); - destinationConnector.setDestinationName(TestUtils.DEFAULT_DESTINATION_NAME); - destinationConnector.setInboundDataType(new TestDataType()); - destinationConnector.setOutboundDataType(new TestDataType()); - destinationConnector.setResponseTransformerExecutor(TestUtils.createDefaultResponseTransformerExecutor()); - - Map params = new HashMap(); - params.put("localChannelId", ChannelController.getInstance().getLocalChannelId(channel.getChannelId())); - params.put("channelId", channel.getChannelId()); - params.put("metaDataId", 1); - params.put("status", Status.QUEUED); - - DestinationQueue destinationConnectorQueue = new DestinationQueue(connectorProperties.getDestinationConnectorProperties().getThreadAssignmentVariable(), connectorProperties.getDestinationConnectorProperties().getThreadCount(), connectorProperties.getDestinationConnectorProperties().isRegenerateTemplate(), destinationConnector.getSerializer(), destinationConnector.getMessageMaps()); - destinationConnectorQueue.setDataSource(new ConnectorMessageQueueDataSource(channel.getChannelId(), channel.getServerId(), 1, Status.QUEUED, false, daoFactory)); - destinationConnector.setQueue(destinationConnectorQueue); - - ((TestDispatcher) destinationConnector).setReturnStatus(returnStatus); - - class BlockingTestResponseTransformer extends TestResponseTransformer { - public volatile boolean waiting = true; - - @Override - public String doTransform(Response response, ConnectorMessage connectorMessage) throws DonkeyException, InterruptedException { - while (waiting) { - try { - Thread.sleep(waitTime); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - return super.doTransform(response, connectorMessage); - } - } - - // Create a response transformer that stalls the processing thread a specified amount of time - BlockingTestResponseTransformer responseTransformer = new BlockingTestResponseTransformer(); - destinationConnector.getResponseTransformerExecutor().setResponseTransformer(responseTransformer); - - chain.addDestination(1, destinationConnector); - - channel.addDestinationChainProvider(chain); - - ChannelController.getInstance().deleteAllMessages(channel.getChannelId()); - TestUtils.deleteChannelStatistics(channel.getChannelId()); - channel.deploy(); - channel.start(null); - - Map stats; - - logger.info(String.format("%-140s", "Testing destination connector statistics changing: RECEIVED->QUEUED->PENDING->SENT")); - logger.info(String.format("%-140s", "Test size: " + testSize)); - logger.info(String.format("%-140s", "")); - - logger.info(String.format("%-140s", "Destination Connector Stats")); - logger.info(String.format("%-140s", "---------------------------")); - - // Send messages through the channel - for (long i = 1; i <= testSize; i++) { - destinationConnector.waiting = true; - responseTransformer.waiting = true; - - logger.info(String.format("%-140s", "Sending Message #" + i + "...")); - ((TestSourceConnector) channel.getSourceConnector()).readTestMessage(testMessage); - - logger.info(String.format("%-140s", String.format("%-50s", " - After sending (should be QUEUED): ") + (stats = ChannelController.getInstance().getStatistics().getConnectorStats(channelId, 1)))); - - // Assert that destination connector stats are correct - assertDestinationStatsCorrect(stats, i, returnStatus, Status.QUEUED); - - destinationConnector.waiting = false; - logger.info(String.format("%-140s", " - Waiting " + waitTime + " ms...")); - Thread.sleep(waitTime); - - logger.info(String.format("%-140s", String.format("%-50s", " - After first wait (should be PENDING): ") + (stats = ChannelController.getInstance().getStatistics().getConnectorStats(channelId, 1)))); - - // Assert that destination connector stats are correct - assertDestinationStatsCorrect(stats, i, returnStatus, Status.PENDING); - - responseTransformer.waiting = false; - logger.info(String.format("%-140s", " - Waiting " + waitTime + " ms...")); - Thread.sleep(waitTime); - - logger.info(String.format("%-140s", String.format("%-50s", " - After second wait (should be " + returnStatus + "): ") + (stats = ChannelController.getInstance().getStatistics().getConnectorStats(channelId, 1)))); - - // Assert the source connector stats are correct - statsEqual(channel.getChannelId(), 0, i, 0L, i, 0L, 0L, 0L); - - // Assert that destination connector stats are correct - assertDestinationStatsCorrect(stats, i, returnStatus, returnStatus); - - // Assert the aggregate stats are correct - assertTrue(channelStatsCorrect()); - } - - // Assert that the messages were sent - assertEquals(testSize, channel.getNumMessages()); - - // Assert the source connector stats are correct - statsEqual(channel.getChannelId(), 0, testSize, 0L, testSize, 0L, 0L, 0L); - // Assert that destination connector stats are correct - destinationStatsCorrect(channel.getChannelId(), 1, testSize, returnStatus, returnStatus); - // Assert the aggregate stats are correct - assertTrue(channelStatsCorrect()); - - channel.stop(); - channel.undeploy(); - - ChannelController.getInstance().removeChannel(channel.getChannelId()); - } - - private void statsEqual(String channelId, Integer metaDataId, Long received, Long filtered, Long transformed, Long pending, Long sent, Long error) { - assertStatsEqual(ChannelController.getInstance().getStatistics().getConnectorStats(channelId, metaDataId), received, filtered, transformed, pending, sent, error); - } - - private void assertStatsEqual(Map stats, Long received, Long filtered, Long transformed, Long pending, Long sent, Long error) { - assertEquals(received, stats.get(Status.RECEIVED)); - assertEquals(filtered, stats.get(Status.FILTERED)); - assertEquals(transformed, stats.get(Status.TRANSFORMED)); - assertEquals(pending, stats.get(Status.PENDING)); - assertEquals(sent, stats.get(Status.SENT)); - assertEquals(error, stats.get(Status.ERROR)); - } - - /* - * Gets the aggregate channel statistics from storage and checks whether they are correct with - * respect to each individual connector. Although the TRANSFORMED status is updated in the - * channel stats along with both source and destination connectors (in ChannelStatistics), the - * destination connector message status is never updated in the database as TRANSFORMED after it - * passes through the filter/transformer. So here, we only check the aggregate TRANSFORMED - * statistics with respect to the source connector. - */ - private boolean channelStatsCorrect() { - Map> stats = ChannelController.getInstance().getStatistics().getChannelStats(channelId); - Map channelStats = createStatsMap(); - - for (Integer metaDataId : stats.keySet()) { - if (metaDataId != null) { - for (Status status : stats.get(metaDataId).keySet()) { - switch (status) { - // Aggregate RECEIVED and TRANSFORMED stats should be the same as the source connector - case RECEIVED: - case TRANSFORMED: - if (metaDataId == 0) { - channelStats.put(status, channelStats.get(status) + stats.get(metaDataId).get(status)); - } - break; - - // Aggregate FILTERED and ERROR stats should be the same as all connectors combined - case FILTERED: - case ERROR: - channelStats.put(status, channelStats.get(status) + stats.get(metaDataId).get(status)); - break; - - // Aggregate PENDING, SENT, and QUEUED stats should be the same as the destination connector - case PENDING: - case SENT: - case QUEUED: - if (metaDataId > 0) { - channelStats.put(status, channelStats.get(status) + stats.get(metaDataId).get(status)); - } - break; - } - } - } - } - - return channelStats.equals(stats.get(null)); - } - - private void destinationStatsCorrect(String channelId, int metaDataId, long numMessages, Status returnStatus, Status currentStatus) { - assertDestinationStatsCorrect(ChannelController.getInstance().getStatistics().getConnectorStats(channelId, metaDataId), numMessages, returnStatus, currentStatus); - } - - private void assertDestinationStatsCorrect(Map stats, long numMessages, Status returnStatus, Status currentStatus) { - long pending = 0; - long remainder = numMessages; - switch (currentStatus) { - case PENDING: - pending = 1; - remainder--; - break; - case QUEUED: - remainder--; - break; - default: - break; - } - - switch (returnStatus) { - case FILTERED: - assertStatsEqual(stats, numMessages, remainder, 0L, pending, 0L, 0L); - break; - - case SENT: - assertStatsEqual(stats, numMessages, 0L, 0L, pending, remainder, 0L); - break; - - case QUEUED: - assertStatsEqual(stats, numMessages, 0L, 0L, pending, 0L, 0L); - break; - - case ERROR: - assertStatsEqual(stats, numMessages, 0L, 0L, pending, 0L, remainder); - break; - } - } - - private Map createStatsMap() { - Map connectorStats = new HashMap(); - connectorStats.put(Status.RECEIVED, 0L); - connectorStats.put(Status.FILTERED, 0L); - connectorStats.put(Status.TRANSFORMED, 0L); - connectorStats.put(Status.PENDING, 0L); - connectorStats.put(Status.SENT, 0L); - connectorStats.put(Status.ERROR, 0L); - - return connectorStats; - } -} \ No newline at end of file diff --git a/server/conf/mirth.properties b/server/conf/mirth.properties index 6b71ebdbf3..e258dd0261 100644 --- a/server/conf/mirth.properties +++ b/server/conf/mirth.properties @@ -80,7 +80,7 @@ database = derby # examples: # Derby jdbc:derby:${dir.appdata}/mirthdb;create=true # PostgreSQL jdbc:postgresql://localhost:5432/mirthdb -# MySQL jdbc:mysql://localhost:3306/mirthdb +# MySQL jdbc:mysql://localhost:3306/mirthdb?sessionVariables=transaction_isolation='READ-COMMITTED' # Oracle jdbc:oracle:thin:@localhost:1521:DB # SQL Server/Sybase (jTDS) jdbc:jtds:sqlserver://localhost:1433/mirthdb # Microsoft SQL Server jdbc:sqlserver://localhost:1433;databaseName=mirthdb diff --git a/server/src/main/java/com/mirth/connect/client/core/Client.java b/server/src/main/java/com/mirth/connect/client/core/Client.java index a927508d67..e5d0a3688e 100644 --- a/server/src/main/java/com/mirth/connect/client/core/Client.java +++ b/server/src/main/java/com/mirth/connect/client/core/Client.java @@ -809,6 +809,16 @@ public void setConfigurationMap(Map map) throws C getServlet(ConfigurationServletInterface.class).setConfigurationMap(map); } + /** + * Updates a single entry in the configuration map, leaving the rest alone. + * + * @see ConfigurationServletInterface#setConfigurationProperty + */ + @Override + public void setConfigurationProperty(String key, ConfigurationProperty property) throws ClientException { + getServlet(ConfigurationServletInterface.class).setConfigurationProperty(key, property); + } + /** * Returns the database driver list. * diff --git a/server/src/main/java/com/mirth/connect/client/core/api/servlets/ConfigurationServletInterface.java b/server/src/main/java/com/mirth/connect/client/core/api/servlets/ConfigurationServletInterface.java index 9f8d391bfe..4c9ca4907e 100644 --- a/server/src/main/java/com/mirth/connect/client/core/api/servlets/ConfigurationServletInterface.java +++ b/server/src/main/java/com/mirth/connect/client/core/api/servlets/ConfigurationServletInterface.java @@ -306,6 +306,14 @@ public void setConfigurationMap(@Param("map") @RequestBody(description = "The ne @Content(mediaType = MediaType.APPLICATION_JSON, examples = { @ExampleObject(name = "configurationMap", ref = "../apiexamples/configuration_map_json") }) }) Map map) throws ClientException; + @PUT + @Path("/configurationMap/{key}") + @Operation(summary = "Updates a single entry in the configuration map, leaving the rest alone.") + @MirthOperation(name = "setConfigurationProperty", display = "Set configuration map entry", permission = Permissions.CONFIGURATION_MAP_EDIT) + public void setConfigurationProperty( + @Param("key") @Parameter(description = "The key of the entry to set.", required = true) @PathParam("key") String key, + @Param("property") @RequestBody(description = "The value and comment to store under the key.", required = true) ConfigurationProperty property) throws ClientException; + @GET @Path("/databaseDrivers") @Operation(summary = "Returns the database driver list.") diff --git a/server/src/main/java/com/mirth/connect/server/api/servlets/ChannelStatisticsServlet.java b/server/src/main/java/com/mirth/connect/server/api/servlets/ChannelStatisticsServlet.java index 48467e6598..6f69628e7d 100644 --- a/server/src/main/java/com/mirth/connect/server/api/servlets/ChannelStatisticsServlet.java +++ b/server/src/main/java/com/mirth/connect/server/api/servlets/ChannelStatisticsServlet.java @@ -105,6 +105,11 @@ public ChannelStatistics getStatistics(String channelId) { if (CollectionUtils.isNotEmpty(channelStatisticsList)) { channelStatistics = channelStatisticsList.get(0); + + // Paranoia: verify that the channel ID matches the requested one + if (!channelId.equals(channelStatistics.getChannelId())) { + throw new IllegalStateException("Channel ID mismatch: expected " + channelId + " but got " + channelStatistics.getChannelId()); + } } else { channelStatistics = new ChannelStatistics(); channelStatistics.setChannelId(channelId); diff --git a/server/src/main/java/com/mirth/connect/server/api/servlets/ConfigurationServlet.java b/server/src/main/java/com/mirth/connect/server/api/servlets/ConfigurationServlet.java index 67dd3419fc..e58740be3a 100644 --- a/server/src/main/java/com/mirth/connect/server/api/servlets/ConfigurationServlet.java +++ b/server/src/main/java/com/mirth/connect/server/api/servlets/ConfigurationServlet.java @@ -306,6 +306,15 @@ public void setConfigurationMap(Map map) { } } + @Override + public void setConfigurationProperty(String key, ConfigurationProperty property) { + try { + configurationController.setConfigurationProperty(key, property); + } catch (ControllerException e) { + throw new MirthApiException(e); + } + } + @Override public List getDatabaseDrivers() { try { diff --git a/server/src/main/java/com/mirth/connect/server/controllers/ChannelController.java b/server/src/main/java/com/mirth/connect/server/controllers/ChannelController.java index b7897771c7..21b9eede6a 100644 --- a/server/src/main/java/com/mirth/connect/server/controllers/ChannelController.java +++ b/server/src/main/java/com/mirth/connect/server/controllers/ChannelController.java @@ -86,8 +86,12 @@ public static ChannelController getInstance() { public abstract Statistics getStatisticsFromStorage(String serverId); + public abstract Statistics getStatisticsFromStorage(String serverId, Set channelIds); + public abstract Statistics getTotalStatisticsFromStorage(String serverId); + public abstract Statistics getTotalStatisticsFromStorage(String serverId, Set channelIds); + public abstract int getConnectorMessageCount(String channelId, String serverId, int metaDataId, Status status); public abstract void resetStatistics(Map> channelConnectorMap, Set statuses); diff --git a/server/src/main/java/com/mirth/connect/server/controllers/ConfigurationController.java b/server/src/main/java/com/mirth/connect/server/controllers/ConfigurationController.java index e4a9ce88c3..7aa1d49dd1 100644 --- a/server/src/main/java/com/mirth/connect/server/controllers/ConfigurationController.java +++ b/server/src/main/java/com/mirth/connect/server/controllers/ConfigurationController.java @@ -328,6 +328,19 @@ public static ConfigurationController getInstance() { */ public abstract void setConfigurationProperties(Map map, boolean persist) throws ControllerException; + /** + * Sets a single configuration map entry, leaving every other entry alone. Doing the + * read-modify-write here rather than in the caller is what makes it atomic: a client can only + * replace the whole map, so two clients updating different keys at once would otherwise drop + * each other's writes. + * + * @param key + * The key of the entry to set. + * @param property + * The value and comment to store under the key. + */ + public abstract void setConfigurationProperty(String key, ConfigurationProperty property) throws ControllerException; + // properties public Properties getPropertiesForGroup(String group) { return getPropertiesForGroup(group, null); diff --git a/server/src/main/java/com/mirth/connect/server/controllers/DefaultChannelController.java b/server/src/main/java/com/mirth/connect/server/controllers/DefaultChannelController.java index 2dba0303e5..b42aeb996f 100644 --- a/server/src/main/java/com/mirth/connect/server/controllers/DefaultChannelController.java +++ b/server/src/main/java/com/mirth/connect/server/controllers/DefaultChannelController.java @@ -732,11 +732,21 @@ public Statistics getStatisticsFromStorage(String serverId) { return com.mirth.connect.donkey.server.controllers.ChannelController.getInstance().getStatisticsFromStorage(serverId); } + @Override + public Statistics getStatisticsFromStorage(String serverId, Set channelIds) { + return com.mirth.connect.donkey.server.controllers.ChannelController.getInstance().getStatisticsFromStorage(serverId, channelIds); + } + @Override public Statistics getTotalStatisticsFromStorage(String serverId) { return com.mirth.connect.donkey.server.controllers.ChannelController.getInstance().getTotalStatisticsFromStorage(serverId); } + @Override + public Statistics getTotalStatisticsFromStorage(String serverId, Set channelIds) { + return com.mirth.connect.donkey.server.controllers.ChannelController.getInstance().getTotalStatisticsFromStorage(serverId, channelIds); + } + @Override public int getConnectorMessageCount(String channelId, String serverId, int metaDataId, Status status) { return com.mirth.connect.donkey.server.controllers.ChannelController.getInstance().getConnectorMessageCount(channelId, serverId, metaDataId, status); diff --git a/server/src/main/java/com/mirth/connect/server/controllers/DefaultConfigurationController.java b/server/src/main/java/com/mirth/connect/server/controllers/DefaultConfigurationController.java index bfee2eddbf..bc01bd9e94 100644 --- a/server/src/main/java/com/mirth/connect/server/controllers/DefaultConfigurationController.java +++ b/server/src/main/java/com/mirth/connect/server/controllers/DefaultConfigurationController.java @@ -938,6 +938,13 @@ public synchronized void setConfigurationProperties(Map properties = getConfigurationProperties(); + properties.put(key, property); + setConfigurationProperties(properties, true); + } + @Override public void setStatus(int status) { this.status = status; diff --git a/server/src/main/java/com/mirth/connect/server/controllers/DonkeyEngineController.java b/server/src/main/java/com/mirth/connect/server/controllers/DonkeyEngineController.java index 1dbeb66b03..19d4a3f947 100644 --- a/server/src/main/java/com/mirth/connect/server/controllers/DonkeyEngineController.java +++ b/server/src/main/java/com/mirth/connect/server/controllers/DonkeyEngineController.java @@ -739,7 +739,7 @@ private Map getDashboardChannels(Set channelIds) { synchronized (deployingChannels) { for (Channel channel : deployingChannels) { - if (!channels.containsKey(channel.getChannelId())) { + if (requested(channelIds, channel) && !channels.containsKey(channel.getChannelId())) { channels.put(channel.getChannelId(), channel); } } @@ -747,7 +747,7 @@ private Map getDashboardChannels(Set channelIds) { synchronized (undeployingChannels) { for (Channel channel : undeployingChannels) { - if (!channels.containsKey(channel.getChannelId())) { + if (requested(channelIds, channel) && !channels.containsKey(channel.getChannelId())) { channels.put(channel.getChannelId(), channel); } } @@ -755,6 +755,11 @@ private Map getDashboardChannels(Set channelIds) { return channels; } + /** Whether one channel is in the set a caller asked about; an empty set means all of them. */ + private boolean requested(Set channelIds, Channel channel) { + return CollectionUtils.isEmpty(channelIds) || channelIds.contains(channel.getChannelId()); + } + @Override public List getChannelStatusList(Set channelIds, boolean includeUndeployed) { List statusList = new ArrayList<>(); @@ -778,8 +783,16 @@ public List getChannelStatusList(Set channelIds, boolea private List getUndeployedDashboardStatuses(Collection channelModels, Map metadataMap) { List statuses = new ArrayList(); - Statistics stats = channelController.getStatisticsFromStorage(configurationController.getServerId()); - Statistics lifetimeStats = channelController.getTotalStatisticsFromStorage(configurationController.getServerId()); + if (channelModels.isEmpty()) { + return statuses; + } + + Set channelIds = new HashSet(); + for (com.mirth.connect.model.Channel channelModel : channelModels) { + channelIds.add(channelModel.getId()); + } + Statistics stats = channelController.getStatisticsFromStorage(configurationController.getServerId(), channelIds); + Statistics lifetimeStats = channelController.getTotalStatisticsFromStorage(configurationController.getServerId(), channelIds); String serverId = configurationController.getServerId(); for (com.mirth.connect.model.Channel channelModel : channelModels) { @@ -1043,7 +1056,15 @@ private List getDashboardChannelStatistics(Collection getUndeployedChannelStatistics(Collection channelModels, Set includeMetaDataIds, Set excludeMetaDataIds) { List statisticsList = new ArrayList(); - Statistics stats = channelController.getStatisticsFromStorage(configurationController.getServerId()); + if (channelModels.isEmpty()) { + return statisticsList; + } + + Set channelIds = new HashSet(); + for (com.mirth.connect.model.Channel channelModel : channelModels) { + channelIds.add(channelModel.getId()); + } + Statistics stats = channelController.getStatisticsFromStorage(configurationController.getServerId(), channelIds); String serverId = configurationController.getServerId(); diff --git a/server/src/test/java/com/mirth/connect/model/converters/DocumentSerailizerTests.java b/server/src/test/java/com/mirth/connect/model/converters/DocumentSerailizerTests.java deleted file mode 100644 index 72e6fa552e..0000000000 --- a/server/src/test/java/com/mirth/connect/model/converters/DocumentSerailizerTests.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) Mirth Corporation. All rights reserved. - * - * http://www.mirthcorp.com - * - * The software in this package is published under the terms of the MPL license a copy of which has - * been included with this distribution in the LICENSE.txt file. - */ - -package com.mirth.connect.model.converters; - -import javax.xml.parsers.DocumentBuilderFactory; - -import junit.framework.Assert; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.w3c.dom.Document; -import org.w3c.dom.Element; - -public class DocumentSerailizerTests { - - @Before - public void setUp() throws Exception {} - - @After - public void tearDown() throws Exception {} - - @Test - public void testToXML() throws Exception { - DocumentSerializer serializer = new DocumentSerializer(); - Document document = getSecureDocumentBuilderFactory(); - - Element element = document.createElement("root"); - element.setTextContent("Hello\r\nworld!"); - document.appendChild(element); - - String actual = serializer.toXML(document); - String expected = "\nHello \nworld!\n"; - Assert.assertEquals(expected, actual); - } - - @Test - public void testPreserveSpace() throws Exception { - Document document = getSecureDocumentBuilderFactory(); - Element root = document.createElement("root"); - document.appendChild(root); - Element child = document.createElement("child"); - child.setTextContent("Hello\nworld!"); - root.appendChild(child); - - DocumentSerializer serializer = new DocumentSerializer(); - String actual = serializer.toXML(document); - String expected = "\n\nHello\nworld!\n\n"; - Assert.assertEquals(expected, actual); - } - - @Test - public void testFromXML() { - - } - - private static Document getSecureDocumentBuilderFactory() throws Exception { - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); - dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); - return dbf.newDocumentBuilder().newDocument(); - } - -} diff --git a/server/src/test/java/com/mirth/connect/model/converters/DocumentSerializerTest.java b/server/src/test/java/com/mirth/connect/model/converters/DocumentSerializerTest.java index 2e8b08a202..1ab7972040 100644 --- a/server/src/test/java/com/mirth/connect/model/converters/DocumentSerializerTest.java +++ b/server/src/test/java/com/mirth/connect/model/converters/DocumentSerializerTest.java @@ -1,5 +1,6 @@ package com.mirth.connect.model.converters; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -22,6 +23,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.w3c.dom.Document; +import org.w3c.dom.Element; import org.xml.sax.InputSource; public class DocumentSerializerTest { @@ -87,6 +89,34 @@ public void testValidFromXML() throws Exception { assertNotNull(document); } + @Test + public void testToXML() throws Exception { + Document document = newDocument(); + Element root = document.createElement("root"); + root.setTextContent("Hello\r\nworld!"); + document.appendChild(root); + + // the carriage return survives as a character reference, the line feed as itself + assertEquals("\nHello \nworld!\n", serializer.toXML(document)); + } + + @Test + public void testPreserveSpace() throws Exception { + Document document = newDocument(); + Element root = document.createElement("root"); + document.appendChild(root); + Element child = document.createElement("child"); + child.setTextContent("Hello\nworld!"); + root.appendChild(child); + + // the element gets indented, but the line feed inside the value is left alone + assertEquals("\n\n Hello\nworld!\n\n", serializer.toXML(document)); + } + + private static Document newDocument() throws Exception { + return DocumentSerializer.getSecureDocumentBuilderFactory().newDocumentBuilder().newDocument(); + } + // Util method for debugging purposes @SuppressWarnings("unused") private static void printDocument(Document doc) throws IOException, TransformerException { diff --git a/server/src/test/java/com/mirth/connect/model/converters/TestUtil.java b/server/src/test/java/com/mirth/connect/model/converters/TestUtil.java index 405550a60f..214e74e7c1 100644 --- a/server/src/test/java/com/mirth/connect/model/converters/TestUtil.java +++ b/server/src/test/java/com/mirth/connect/model/converters/TestUtil.java @@ -49,6 +49,11 @@ public static String prettyPrintXml(String input) throws Exception { return writer.toString(); } + /** Line endings carry no meaning in pretty-printed XML, and the fixtures on disk are CRLF. */ + public static String normalizeLineEndings(String input) { + return input.replaceAll("\r\n|\r", "\n"); + } + public static String convertCRToCRLF(String input) { return input.replaceAll("\r", "\r\n"); } diff --git a/server/src/test/java/com/mirth/connect/model/util/MigrationUtilTests.java b/server/src/test/java/com/mirth/connect/model/util/MigrationUtilTest.java similarity index 72% rename from server/src/test/java/com/mirth/connect/model/util/MigrationUtilTests.java rename to server/src/test/java/com/mirth/connect/model/util/MigrationUtilTest.java index 8935c2abe4..d4145c6165 100644 --- a/server/src/test/java/com/mirth/connect/model/util/MigrationUtilTests.java +++ b/server/src/test/java/com/mirth/connect/model/util/MigrationUtilTest.java @@ -13,7 +13,7 @@ import com.mirth.connect.util.MigrationUtil; -public class MigrationUtilTests extends TestCase { +public class MigrationUtilTest extends TestCase { public void testCompareVersions() { assertEquals(1, MigrationUtil.compareVersions("5", "4")); assertEquals(-1, MigrationUtil.compareVersions("5", "6")); @@ -31,10 +31,15 @@ public void testCompareVersions() { assertEquals(-1, MigrationUtil.compareVersions("1.8", "1.8.2")); } + /* + * length is the exact number of components to emit: shorter versions are padded with zeroes and + * longer ones are truncated, so a length below the number of components present loses them and + * a non-positive length yields nothing at all. Both production callers pass 3. + */ public void testNormalizeVersion() { - assertEquals("1.8", MigrationUtil.normalizeVersion("1.8", -1)); - assertEquals("1.8", MigrationUtil.normalizeVersion("1.8", 0)); - assertEquals("1.8", MigrationUtil.normalizeVersion("1.8", 1)); + assertEquals("", MigrationUtil.normalizeVersion("1.8", -1)); + assertEquals("", MigrationUtil.normalizeVersion("1.8", 0)); + assertEquals("1", MigrationUtil.normalizeVersion("1.8", 1)); assertEquals("1.8", MigrationUtil.normalizeVersion("1.8", 2)); assertEquals("1.8.0", MigrationUtil.normalizeVersion("1.8", 3)); assertEquals("1.8.0.0", MigrationUtil.normalizeVersion("1.8", 4)); diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/dicom/DICOMSerializerTest.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/dicom/DICOMSerializerTest.java new file mode 100644 index 0000000000..5309cdc172 --- /dev/null +++ b/server/src/test/java/com/mirth/connect/plugins/datatypes/dicom/DICOMSerializerTest.java @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2017 Mirth Corporation +// SPDX-FileCopyrightText: 2026 Mitch Gaffigan + +package com.mirth.connect.plugins.datatypes.dicom; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +/** + * Serializes every sample in tests/dicom and compares it against the committed XML, which is the + * serializer's own output and so is already indented. Drop a new .dcm and its .xml into that + * directory and it is picked up without touching this class; see the README there for provenance. + */ +@RunWith(Parameterized.class) +public class DICOMSerializerTest { + + // server/build.gradle runs tests with workingDir = projectDir + private static final File SAMPLES = new File("tests/dicom"); + + @Parameters(name = "{0}") + public static List samples() { + String[] names = SAMPLES.list((dir, name) -> name.endsWith(".dcm")); + assertTrue("No DICOM samples found in " + SAMPLES.getAbsolutePath(), names != null && names.length > 0); + Arrays.sort(names); + return Arrays.asList(names); + } + + @Parameter + public String name; + + @Test + public void toXml() throws Exception { + // the committed XML is of the header alone; pixel data would dwarf it + byte[] header = DICOMSerializer.removePixelData(FileUtils.readFileToByteArray(new File(SAMPLES, name))); + String actual = new DICOMSerializer().toXML(Base64.getEncoder().encodeToString(header)); + String expected = FileUtils.readFileToString(new File(SAMPLES, StringUtils.removeEnd(name, ".dcm") + ".xml"), UTF_8); + + assertEquals(expected, actual); + } +} diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/dicom/DICOMSerializerTests.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/dicom/DICOMSerializerTests.java deleted file mode 100644 index db549d4948..0000000000 --- a/server/src/test/java/com/mirth/connect/plugins/datatypes/dicom/DICOMSerializerTests.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) Mirth Corporation. All rights reserved. - * - * http://www.mirthcorp.com - * - * The software in this package is published under the terms of the MPL license a copy of which has - * been included with this distribution in the LICENSE.txt file. - */ - -package com.mirth.connect.plugins.datatypes.dicom; - -import java.io.File; - -import junit.framework.Assert; - -import org.apache.commons.io.FileUtils; -import org.apache.commons.net.util.Base64; -import org.junit.Test; - -import com.mirth.connect.model.converters.TestUtil; - -public class DICOMSerializerTests { - @Test - public void testToXml1() throws Exception { - String input = Base64.encodeBase64String(FileUtils.readFileToByteArray(new File("tests/test-dicom-input-1.dcm"))); - String output = FileUtils.readFileToString(new File("tests/test-dicom-output-1.xml")); - DICOMSerializer serializer = new DICOMSerializer(); - Assert.assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); - } - - @Test - public void testToXml2() throws Exception { - String input = Base64.encodeBase64String(FileUtils.readFileToByteArray(new File("tests/test-dicom-input-2.dcm"))); - String output = FileUtils.readFileToString(new File("tests/test-dicom-output-2.xml")); - DICOMSerializer serializer = new DICOMSerializer(); - Assert.assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); - } - - @Test - public void testToXml3() throws Exception { - String input = Base64.encodeBase64String(FileUtils.readFileToByteArray(new File("tests/test-dicom-input-3.dcm"))); - String output = FileUtils.readFileToString(new File("tests/test-dicom-output-3.xml")); - DICOMSerializer serializer = new DICOMSerializer(); - Assert.assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); - } -} \ No newline at end of file diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/dicom/DICOMTests.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/dicom/DICOMTests.java deleted file mode 100644 index c921f3b496..0000000000 --- a/server/src/test/java/com/mirth/connect/plugins/datatypes/dicom/DICOMTests.java +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright (c) Mirth Corporation. All rights reserved. - * - * http://www.mirthcorp.com - * - * The software in this package is published under the terms of the MPL license a copy of which has - * been included with this distribution in the LICENSE.txt file. - */ - -package com.mirth.connect.plugins.datatypes.dicom; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; - -import org.xml.sax.SAXException; - -import com.mirth.connect.donkey.model.message.MessageSerializerException; -import com.mirth.connect.model.converters.Stopwatch; - -/** - * Created by IntelliJ IDEA. User: dans Date: Aug 6, 2007 Time: 2:00:06 PM To change this template - * use File | Settings | File Templates. - */ -public class DICOMTests { - public static void main(String[] args) { - String testMessage = ""; - ArrayList testFiles = new ArrayList(); - testFiles.add("C:\\abdominal.dcm"); - //testFiles.add("C:\\abdominal.dcm"); - //testFiles.add("C:\\brain.dcm"); - //String[] a = new String[1]; - //a[0] = "c:\\ankle.dcm"; - - //ImageJ ij = new ImageJ(null,ImageJ.EMBEDDED); - //ImageJ.main(a); - -// Client client = new Client("https://localhost:8443"); -// Attachment a = new Attachment(); -// a.setAttachmentId("TEST1"); -// a.setMessageId("TEST2"); -// a.setSize(100); -// a.setType("TESTING"); -// try { -// client.login("admin", "admin", "1.5.0"); -// a.setData(getBytesFromFile(new File("C:\\abdominal.dcm"))); -//// client.insertAttachment(a); -// -// Attachment a1 = client.getAttachment("TEST1"); -// List a3 = client.getAttachmentsByMessageId("TEST2"); -// Attachment a2 = a3.get(0); -// if(a1.equals(a2)){ -// System.out.println("They are the same"); -// } -// System.out.println("First:" + a1.toString()); -// -// System.out.println("Second:" + a2.toString()); -// -// -// } -// catch(Exception e) { -// e.printStackTrace(); -// } - -// Iterator iterator = testFiles.iterator(); -// while(iterator.hasNext()){ -// String fileName = (String) iterator.next(); -// try { -// testMessage = new String(getBytesFromFile(new File(fileName))); -// System.out.println("Processing test file:" + fileName); -// //System.out.println(testMessage); -// } catch (IOException e) { -// e.printStackTrace(); -// } -// try { -// -// long totalExecutionTime = 0; -// int iterations = 1; -// for (int i = 0; i < iterations; i++) { -// totalExecutionTime+=runTest(testMessage); -// } -// -// //System.out.println("Execution time average: " + totalExecutionTime/iterations + " ms"); -// } -// // System.out.println(new X12Serializer().toXML("SEG*1*2**4*5")); -// catch (SAXException e) { -// e.printStackTrace(); -// } catch (Exception e) { -// e.printStackTrace(); -// } -// } - } - - private static long runTest(String testMessage) throws MessageSerializerException, SAXException, IOException { - Stopwatch stopwatch = new Stopwatch(); -// Properties properties = new Properties(); -// properties.put("includePixelData","no"); -// properties.put("isEncoded","no"); - stopwatch.start(); - DICOMSerializer serializer = new DICOMSerializer(null); -// String xmloutput = serializer.toXML(testMessage); - //Dcm2Xml dcm2xml = new Dcm2Xml(); - File xmlOut = File.createTempFile("test", "xml"); - File dcmInput = new File("c:\\US-PAL-8-10x-echo.dcm"); - try { - // dcm2xml.convert(dcmInput,xmlOut); - } catch (Exception e) { - e.printStackTrace(); - } - File dcmOutput = File.createTempFile("test", "dcm"); - String[] args = new String[4]; - args[0] = "-x"; - args[1] = xmlOut.getAbsolutePath(); - args[2] = "-o"; - args[3] = "c:\\dcmOutput.dcm"; - //Xml2Dcm.main(args); - // TO XML again - File input2 = new File("c:\\dcmOutput.dcm"); - try { - /// dcm2xml.convert(input2,xmlOut); - } catch (Exception e) { - e.printStackTrace(); - } - dcmOutput = File.createTempFile("test", "dcm"); - args = new String[4]; - args[0] = "-x"; - args[1] = xmlOut.getAbsolutePath(); - args[2] = "-o"; - args[3] = "c:\\dcmOutput2.dcm"; - // Xml2Dcm.main(args); - //System.out.println(xmloutput); -// DocumentSerializer docser = new DocumentSerializer(); -// docser.setPreserveSpace(true); -// -// Document doc = docser.fromXML(xmloutput); -// XMLReader xr = XMLReaderFactory.createXMLReader(); - String results = ""; //= serializer.fromXML(xmloutput); -// String xmloutput2 = serializer.toXML(results); -// String results2 = serializer.fromXML(xmloutput2); - System.out.println("testing..."); - if (results.replace('\n', '\r').trim().equals(testMessage.replaceAll("\\r\\n", "\r").trim())) { - System.out.println("Test Successful!"); - } else { - String original = testMessage.replaceAll("\\r\\n", "\r").trim(); - String newm = results.replace('\n', '\r').trim(); - for (int i = 0; i < original.length(); i++) { - if (original.charAt(i) == newm.charAt(i)) { - System.out.print(newm.charAt(i)); - } else { - System.out.println(""); - System.out.print("Saw: "); - System.out.println(newm.charAt(i)); - System.out.print("Expected: "); - System.out.print(original.charAt(i)); - break; - } - } - System.out.println("Test Failed!"); - } - return stopwatch.toValue(); - } - - // Returns the contents of the file in a byte array. - private static byte[] getBytesFromFile(File file) throws IOException { - InputStream is = new FileInputStream(file); - - // Get the size of the file - long length = file.length(); - - // You cannot create an array using a long type. - // It needs to be an int type. - // Before converting to an int type, check - // to ensure that file is not larger than Integer.MAX_VALUE. - if (length > Integer.MAX_VALUE) { - // File is too large - } - - // Create the byte array to hold the data - byte[] bytes = new byte[(int) length]; - - // Read in the bytes - int offset = 0; - int numRead = 0; - while (offset < bytes.length && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) { - offset += numRead; - } - - // Ensure all the bytes have been read in - if (offset < bytes.length) { - throw new IOException("Could not completely read file " + file.getName()); - } - - // Close the input stream and return bytes - is.close(); - return bytes; - } -} diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/edi/EDISerializerTest.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/edi/EDISerializerTest.java index 030158e228..7c1819a7e2 100644 --- a/server/src/test/java/com/mirth/connect/plugins/datatypes/edi/EDISerializerTest.java +++ b/server/src/test/java/com/mirth/connect/plugins/datatypes/edi/EDISerializerTest.java @@ -4,11 +4,15 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import java.io.File; + +import org.apache.commons.io.FileUtils; import org.junit.Before; import org.junit.Test; import org.xml.sax.SAXParseException; import com.mirth.connect.donkey.model.message.MessageSerializerException; +import com.mirth.connect.model.converters.TestUtil; import com.mirth.connect.model.datatype.SerializerProperties; public class EDISerializerTest { @@ -41,6 +45,36 @@ public void testFromXMLWithExternalDtd() { assertTrue(exceptionCaught); } + @Test + public void testToXml() throws Exception { + assertXmlEquals(read("test-edi-output.xml"), serializer.toXML(read("test-edi-input.txt"))); + } + + @Test + public void testFromXml() throws Exception { + assertEquals(read("test-edi-input.txt"), serializer.fromXML(read("test-edi-output.xml"))); + } + + /** Issue 1597: the serializer fills in elements the XML leaves out. */ + @Test + public void testIssue1597fromXML() throws Exception { + assertEquals(read("test-1597-output.txt"), serializer.fromXML(read("test-1597-input-missing-elements.xml"))); + } + + @Test + public void testIssue1597toXML() throws Exception { + assertXmlEquals(read("test-1597-input.xml"), serializer.toXML(read("test-1597-output.txt"))); + } + + private static String read(String name) throws Exception { + return FileUtils.readFileToString(new File("tests/" + name), "UTF-8"); + } + + /** toXML returns one unindented line with an XML declaration; the fixtures are readable. */ + private static void assertXmlEquals(String expected, String actual) throws Exception { + assertEquals(TestUtil.normalizeLineEndings(expected), TestUtil.normalizeLineEndings(TestUtil.prettyPrintXml(actual))); + } + private static String validXml = "\r\n" + "bar"; diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/edi/EDISerializerTests.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/edi/EDISerializerTests.java deleted file mode 100644 index 3969ceaad8..0000000000 --- a/server/src/test/java/com/mirth/connect/plugins/datatypes/edi/EDISerializerTests.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) Mirth Corporation. All rights reserved. - * - * http://www.mirthcorp.com - * - * The software in this package is published under the terms of the MPL license a copy of which has - * been included with this distribution in the LICENSE.txt file. - */ - -package com.mirth.connect.plugins.datatypes.edi; - -import java.io.File; - -import junit.framework.Assert; - -import org.apache.commons.io.FileUtils; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -public class EDISerializerTests { - @Before - public void setUp() throws Exception { - - } - - @After - public void tearDown() throws Exception { - - } - - @Test - public void testToXml() throws Exception { - String input = FileUtils.readFileToString(new File("tests/test-edi-input.txt"), "UTF-8"); - String output = FileUtils.readFileToString(new File("tests/test-edi-output.xml"), "UTF-8"); - EDISerializer serializer = new EDISerializer(new EDIDataTypeProperties().getSerializerProperties()); - Assert.assertEquals(output, serializer.toXML(input)); - } - - @Test - public void testFromXml() throws Exception { - String input = FileUtils.readFileToString(new File("tests/test-edi-output.xml"), "UTF-8"); - String output = FileUtils.readFileToString(new File("tests/test-edi-input.txt"), "UTF-8"); - EDISerializer serializer = new EDISerializer(new EDIDataTypeProperties().getSerializerProperties()); - Assert.assertEquals(output, serializer.fromXML(input)); - } - - /* - * Checks if serializer adds missing elements when going from XML to EDI/X12. - */ - @Test - public void testIssue1597fromXML() throws Exception { - String input = FileUtils.readFileToString(new File("tests/test-1597-input-missing-elements.xml"), "UTF-8"); - String output = FileUtils.readFileToString(new File("tests/test-1597-output.txt"), "UTF-8"); - EDISerializer serializer = new EDISerializer(new EDIDataTypeProperties().getSerializerProperties()); - Assert.assertEquals(output, serializer.fromXML(input)); - } - - @Test - public void testIssue1597toXML() throws Exception { - String input = FileUtils.readFileToString(new File("tests/test-1597-output.txt"), "UTF-8"); - String output = FileUtils.readFileToString(new File("tests/test-1597-input.xml"), "UTF-8"); - EDISerializer serializer = new EDISerializer(new EDIDataTypeProperties().getSerializerProperties()); - Assert.assertEquals(output, serializer.toXML(input)); - } -} diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/edi/X12Tests.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/edi/X12Tests.java deleted file mode 100644 index 592b80bcdd..0000000000 --- a/server/src/test/java/com/mirth/connect/plugins/datatypes/edi/X12Tests.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright (c) Mirth Corporation. All rights reserved. - * - * http://www.mirthcorp.com - * - * The software in this package is published under the terms of the MPL license a copy of which has - * been included with this distribution in the LICENSE.txt file. - */ - -package com.mirth.connect.plugins.datatypes.edi; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; - -import junit.framework.Assert; - -import org.xml.sax.SAXException; - -import com.mirth.connect.donkey.model.message.MessageSerializerException; -import com.mirth.connect.model.converters.DocumentSerializer; -import com.mirth.connect.model.converters.Stopwatch; - -public class X12Tests { - public static void main(String[] args) { - String testMessage = ""; - try { - testMessage = new String(getBytesFromFile(new File(args[0]))); - System.out.println(testMessage); - } catch (IOException e) { - e.printStackTrace(); - } - try { - - long totalExecutionTime = 0; - int iterations = 100; - for (int i = 0; i < iterations; i++) { - totalExecutionTime += runTest(testMessage); - } - - System.out.println("Execution time average: " + totalExecutionTime / iterations + " ms"); - } - // System.out.println(new X12Serializer().serialize("SEG*1*2**4*5")); - catch (SAXException e) { - e.printStackTrace(); - } catch (Exception e) { - e.printStackTrace(); - } - - } - - private static long runTest(String testMessage) throws MessageSerializerException, SAXException, IOException { - Stopwatch stopwatch = new Stopwatch(); - stopwatch.start(); - EDISerializer serializer = new EDISerializer((new EDIDataTypeProperties()).getSerializerProperties()); - String xmloutput = serializer.toXML(testMessage); - DocumentSerializer docser = new DocumentSerializer(); - String x12 = serializer.fromXML(xmloutput); - stopwatch.stop(); - - // System.out.println(docser.serialize(doc)); // handler.getOutput()); - // System.out.println(x12); - Assert.assertTrue(x12.replace('\n', '\r').trim().equals(testMessage.replaceAll("\\r\\n", "\r").trim())); - - if (x12.replace('\n', '\r').trim().equals(testMessage.replaceAll("\\r\\n", "\r").trim())) { - System.out.println("Test Successful!"); - } else { - String original = testMessage.replaceAll("\\r\\n", "\r").trim(); - String newm = x12.replace('\n', '\r').trim(); - for (int i = 0; i < original.length(); i++) { - if (original.charAt(i) == newm.charAt(i)) { - System.out.print(newm.charAt(i)); - } else { - System.out.println(""); - System.out.print("Saw: "); - System.out.println(newm.charAt(i)); - System.out.print("Expected: "); - System.out.print(original.charAt(i)); - break; - } - } - System.out.println("Test Failed!"); - } - return stopwatch.toValue(); - } - - // Returns the contents of the file in a byte array. - private static byte[] getBytesFromFile(File file) throws IOException { - InputStream is = new FileInputStream(file); - try { - // Get the size of the file - long length = file.length(); - - // You cannot create an array using a long type. - // It needs to be an int type. - // Before converting to an int type, check - // to ensure that file is not larger than Integer.MAX_VALUE. - if (length > Integer.MAX_VALUE) { - // File is too large - } - - // Create the byte array to hold the data - byte[] bytes = new byte[(int) length]; - - // Read in the bytes - int offset = 0; - int numRead = 0; - while (offset < bytes.length && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) { - offset += numRead; - } - - // Ensure all the bytes have been read in - if (offset < bytes.length) { - throw new IOException("Could not completely read file " + file.getName()); - } - return bytes; - } finally { - // Close the input stream and return bytes - is.close(); - - } - } -} diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/edi/X12Tests2.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/edi/X12Tests2.java deleted file mode 100644 index 07a5dc0ec8..0000000000 --- a/server/src/test/java/com/mirth/connect/plugins/datatypes/edi/X12Tests2.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (c) Mirth Corporation. All rights reserved. - * - * http://www.mirthcorp.com - * - * The software in this package is published under the terms of the MPL license a copy of which has - * been included with this distribution in the LICENSE.txt file. - */ - -package com.mirth.connect.plugins.datatypes.edi; - -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Map.Entry; - -import javax.xml.bind.JAXBContext; -import javax.xml.bind.JAXBElement; -import javax.xml.bind.Unmarshaller; - -public class X12Tests2 { - - /** - * @param args - */ - public static void main(String[] args) { - try { - JAXBContext jc = JAXBContext.newInstance("com.mirth.connect.model.edi"); - Unmarshaller unmarshaller = jc.createUnmarshaller(); - // TransactionType collection= - // (TransactionType)((JAXBElement)unmarshaller.unmarshal(X12Test.class.getResourceAsStream("xml/837.4010.X097.xml"))).getValue(); - TransactionType collection = (TransactionType) ((JAXBElement) unmarshaller.unmarshal(X12Tests2.class.getResourceAsStream("xml/997.4010.xml"))).getValue(); - System.out.println(collection.getId() + ": " + collection.getName()); - - HashMap mappings = new LinkedHashMap(); - LoopType loop = collection.getLoop(); - processLoop(loop, mappings); - for (Iterator iter = mappings.entrySet().iterator(); iter.hasNext();) { - Entry element = (Entry) iter.next(); - System.out.println(element.getKey() + ": " + element.getValue()); - } - - } catch (Exception e) { - e.printStackTrace(); - } - } - - private static void addEntry(String name, String description, Map mappings) { - // hack way to format for our ui - name = name.replaceAll("_LOOP", "").replaceAll("-", ""); - if (name.length() > 5) { - name = name.substring(0, name.length() - 4) + "." + trimLeadingZero(name.substring(name.length() - 4, name.length() - 2)) + "." + trimLeadingZero(name.substring(name.length() - 2)); - } else if (name.length() > 3) { - name = name.substring(0, name.length() - 2) + "." + trimLeadingZero(name.substring(name.length() - 2)); - } - mappings.put(name.replace('-', '.'), description); - } - - private static String trimLeadingZero(String name) { - if (name.startsWith("0")) { - return name.substring(1); - } else { - return name; - } - } - - private static void processLoop(LoopType loop, Map mappings) { - addEntry(loop.getXid(), loop.getName(), mappings); - for (Iterator iter = loop.getSegmentOrLoopOrRepeat().iterator(); iter.hasNext();) { - Object element = ((JAXBElement) iter.next()).getValue(); - if (element instanceof SegmentType) { - SegmentType segment = (SegmentType) element; - processSegment(segment, mappings); - } else if (element instanceof LoopType) { - processLoop((LoopType) element, mappings); - } - } - } - - private static void processSegment(SegmentType segment, Map mappings) { - addEntry(segment.getXid(), segment.getName(), mappings); - for (Iterator iter = segment.getElementOrComposite().iterator(); iter.hasNext();) { - Object component = iter.next(); - if (component instanceof CompositeType) { - processComposite((CompositeType) component, mappings); - } else if (component instanceof ElementType) { - ElementType element = (ElementType) component; - addEntry(element.getXid(), element.getName(), mappings); - } - } - } - - private static void processComposite(CompositeType composite, Map mappings) { - addEntry(composite.getDataEle(), composite.getName(), mappings); - for (Iterator iter = composite.getElement().iterator(); iter.hasNext();) { - ElementType element = iter.next(); - addEntry(element.getXid(), element.getName(), mappings); - - } - } -} diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/HL7SerializerTests.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/HL7SerializerTest.java similarity index 70% rename from server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/HL7SerializerTests.java rename to server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/HL7SerializerTest.java index aae28422bb..46792bc61f 100644 --- a/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/HL7SerializerTests.java +++ b/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/HL7SerializerTest.java @@ -11,27 +11,19 @@ import java.io.File; -import junit.framework.Assert; - import org.apache.commons.io.FileUtils; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import com.mirth.connect.model.converters.TestUtil; -public class HL7SerializerTests { +public class HL7SerializerTest { private HL7v2DataTypeProperties defaultProperties; @Before public void setUp() throws Exception { - defaultProperties = new HL7v2DataTypeProperties(); - -// defaultProperties = new Properties(); -// defaultProperties.put("useStrictParser", "false"); -// defaultProperties.put("handleRepetitions", "false"); -// defaultProperties.put("handleSubcomponents", "false"); -// defaultProperties.put("inputSegmentDelimiter", "\\r\\n|\\r|\\n"); -// defaultProperties.put("outputSegmentDelimiter", "\\r"); + defaultProperties = properties(false, false); } @Test @@ -39,7 +31,7 @@ public void testToXmlDefault() throws Exception { String input = FileUtils.readFileToString(new File("tests/test-hl7-input.txt")); String output = FileUtils.readFileToString(new File("tests/test-hl7-output.xml")); ER7Serializer serializer = new ER7Serializer(defaultProperties.getSerializerProperties()); - Assert.assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); + assertXmlEquals(output, serializer.toXML(input)); } @Test @@ -55,7 +47,7 @@ public void testToXmlWhitepsace() throws Exception { String input = FileUtils.readFileToString(new File("tests/test-hl7-whitespace-input.txt")); String output = FileUtils.readFileToString(new File("tests/test-hl7-whitespace-output.xml")); ER7Serializer serializer = new ER7Serializer(defaultProperties.getSerializerProperties()); - Assert.assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); + assertXmlEquals(output, serializer.toXML(input)); } @Test @@ -97,12 +89,7 @@ public void testFromXmlMissingSubcomponents() throws Exception { @Test public void testFromXmlSingleSegment() throws Exception { - HL7v2DataTypeProperties properties = new HL7v2DataTypeProperties(); -// properties.put("useStrictParser", "false"); -// properties.put("handleRepetitions", "false"); -// properties.put("handleSubcomponents", "true"); -// properties.put("inputSegmentDelimiter", "\\r\\n|\\r|\\n"); -// properties.put("outputSegmentDelimiter", "\\r"); + HL7v2DataTypeProperties properties = properties(false, true); String input = FileUtils.readFileToString(new File("tests/test-hl7-single-segment-input.xml")); String output = FileUtils.readFileToString(new File("tests/test-hl7-single-segment-output.txt")); @@ -120,29 +107,17 @@ public void testFromXmlSingleField() throws Exception { @Test public void testToXmlWithSubcomponents() throws Exception { - HL7v2DataTypeProperties properties = new HL7v2DataTypeProperties(); -// Properties properties = new Properties(); -// properties.put("useStrictParser", "false"); -// properties.put("handleRepetitions", "false"); -// properties.put("handleSubcomponents", "true"); -// properties.put("inputSegmentDelimiter", "\\r\\n|\\r|\\n"); -// properties.put("outputSegmentDelimiter", "\\r"); + HL7v2DataTypeProperties properties = properties(false, true); String input = FileUtils.readFileToString(new File("tests/test-hl7-subcomponents-input.txt")); String output = FileUtils.readFileToString(new File("tests/test-hl7-subcomponents-output.xml")); ER7Serializer serializer = new ER7Serializer(properties.getSerializerProperties()); - Assert.assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); + assertXmlEquals(output, serializer.toXML(input)); } @Test public void testFromXmlWithSubcomponents() throws Exception { - HL7v2DataTypeProperties properties = new HL7v2DataTypeProperties(); -// Properties properties = new Properties(); -// properties.put("useStrictParser", "false"); -// properties.put("handleRepetitions", "false"); -// properties.put("handleSubcomponents", "true"); -// properties.put("inputSegmentDelimiter", "\\r\\n|\\r|\\n"); -// properties.put("outputSegmentDelimiter", "\\r"); + HL7v2DataTypeProperties properties = properties(false, true); String input = FileUtils.readFileToString(new File("tests/test-hl7-subcomponents-output.xml")); String output = FileUtils.readFileToString(new File("tests/test-hl7-subcomponents-input.txt")); @@ -152,29 +127,17 @@ public void testFromXmlWithSubcomponents() throws Exception { @Test public void testToXmlWithRepetitions() throws Exception { - HL7v2DataTypeProperties properties = new HL7v2DataTypeProperties(); -// Properties properties = new Properties(); -// properties.put("useStrictParser", "false"); -// properties.put("handleRepetitions", "true"); -// properties.put("handleSubcomponents", "false"); -// properties.put("inputSegmentDelimiter", "\\r\\n|\\r|\\n"); -// properties.put("outputSegmentDelimiter", "\\r"); + HL7v2DataTypeProperties properties = properties(true, false); String input = FileUtils.readFileToString(new File("tests/test-hl7-repetitions-input.txt")); String output = FileUtils.readFileToString(new File("tests/test-hl7-repetitions-output.xml")); ER7Serializer serializer = new ER7Serializer(properties.getSerializerProperties()); - Assert.assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); + assertXmlEquals(output, serializer.toXML(input)); } @Test public void testFromXmlWithRepetitions() throws Exception { - HL7v2DataTypeProperties properties = new HL7v2DataTypeProperties(); -// Properties properties = new Properties(); -// properties.put("useStrictParser", "false"); -// properties.put("handleRepetitions", "true"); -// properties.put("handleSubcomponents", "false"); -// properties.put("inputSegmentDelimiter", "\\r\\n|\\r|\\n"); -// properties.put("outputSegmentDelimiter", "\\r"); + HL7v2DataTypeProperties properties = properties(true, false); String input = FileUtils.readFileToString(new File("tests/test-hl7-repetitions-output.xml")); String output = FileUtils.readFileToString(new File("tests/test-hl7-repetitions-input.txt")); @@ -187,7 +150,7 @@ public void testToXmlWithBatch() throws Exception { String input = FileUtils.readFileToString(new File("tests/test-hl7-batch-input.txt")); String output = FileUtils.readFileToString(new File("tests/test-hl7-batch-output.xml")); ER7Serializer serializer = new ER7Serializer(defaultProperties.getSerializerProperties()); - Assert.assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); + assertXmlEquals(output, serializer.toXML(input)); } @Test @@ -197,4 +160,20 @@ public void testFromXmlWithBatch() throws Exception { ER7Serializer serializer = new ER7Serializer(defaultProperties.getSerializerProperties()); Assert.assertEquals(output, TestUtil.convertCRToCRLF(serializer.fromXML(input))); } + + private static void assertXmlEquals(String expected, String actual) throws Exception { + Assert.assertEquals(TestUtil.normalizeLineEndings(expected), TestUtil.normalizeLineEndings(TestUtil.prettyPrintXml(actual))); + } + + /** + * The fixtures predate the typed property classes, which default both flags to true; the + * combination each test was written against survives only in its setup. + */ + private static HL7v2DataTypeProperties properties(boolean handleRepetitions, boolean handleSubcomponents) { + HL7v2DataTypeProperties properties = new HL7v2DataTypeProperties(); + HL7v2SerializationProperties serializationProperties = (HL7v2SerializationProperties) properties.getSerializationProperties(); + serializationProperties.setHandleRepetitions(handleRepetitions); + serializationProperties.setHandleSubcomponents(handleSubcomponents); + return properties; + } } diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/HL7Tests.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/HL7Tests.java deleted file mode 100644 index 343d494e9c..0000000000 --- a/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/HL7Tests.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (c) Mirth Corporation. All rights reserved. - * - * http://www.mirthcorp.com - * - * The software in this package is published under the terms of the MPL license a copy of which has - * been included with this distribution in the LICENSE.txt file. - */ - -package com.mirth.connect.plugins.datatypes.hl7v2; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; - -import org.w3c.dom.Document; -import org.xml.sax.SAXException; - -import com.mirth.connect.donkey.model.message.MessageSerializerException; -import com.mirth.connect.model.converters.DocumentSerializer; -import com.mirth.connect.model.converters.Stopwatch; - -public class HL7Tests { - - public static void main(String[] args) { - - String testMessage = ""; - String testXML = null; - try { - testMessage = new String(getBytesFromFile(new File(args[0]))); - if (args.length > 1) { - testXML = new String(getBytesFromFile(new File(args[1]))); - } - System.out.println(testMessage); - } catch (IOException e) { - e.printStackTrace(); - } - try { - - long totalExecutionTime = 0; - int iterations = 1; - for (int i = 0; i < iterations; i++) { - totalExecutionTime += runTest(testMessage, testXML); - } - - System.out.println("Execution time average: " + totalExecutionTime / iterations + " ms"); - } - // System.out.println(new X12Serializer().serialize("SEG*1*2**4*5")); - catch (SAXException e) { - e.printStackTrace(); - } catch (Exception e) { - e.printStackTrace(); - } - - } - - private static long runTest(String testMessage, String xml) throws MessageSerializerException, SAXException, IOException { - Stopwatch stopwatch = new Stopwatch(); -// Properties properties = new Properties(); -// properties.put("useStrictParser", "false"); -// properties.put("handleRepetitions", "true"); -// properties.put("inputSegmentDelimiter", "\r\n|\r|\n"); -// properties.put("outputSegmentDelimiter", "\r"); - stopwatch.start(); - ER7Serializer serializer = new ER7Serializer(null); - String xmloutput = xml; - String er7 = ""; - if (xml == null) { - xmloutput = serializer.toXML(testMessage); - er7 = serializer.fromXML(xmloutput); - stopwatch.stop(); - } else { - - DocumentSerializer docser = new DocumentSerializer(); - Document doc = docser.fromXML(xmloutput); - er7 = serializer.fromXML(docser.toXML(doc)); - stopwatch.stop(); - } - - //System.out.println(xmloutput); - DocumentSerializer docser = new DocumentSerializer(); - Document doc = docser.fromXML(xmloutput); - - System.out.println(docser.toXML(doc)); - System.out.println(er7); - if (er7.trim().equals(testMessage.trim())) { - System.out.println("Test Successful!"); - } else { - String original = testMessage.replaceAll("\\r\\n", "\r").trim(); - String newm = er7.replace('\n', '\r').trim(); - for (int i = 0; i < original.length(); i++) { - if (original.charAt(i) == newm.charAt(i)) { - System.out.print(newm.charAt(i)); - } else { - System.out.println(""); - System.out.print("Saw: "); - System.out.println(newm.charAt(i)); - - System.out.print("Expected: "); - System.out.print(original.charAt(i)); - break; - } - } - System.out.println("\nTest Failed!"); - } - return stopwatch.toValue(); - } - - // Returns the contents of the file in a byte array. - private static byte[] getBytesFromFile(File file) throws IOException { - InputStream is = new FileInputStream(file); - - // Get the size of the file - long length = file.length(); - - // You cannot create an array using a long type. - // It needs to be an int type. - // Before converting to an int type, check - // to ensure that file is not larger than Integer.MAX_VALUE. - if (length > Integer.MAX_VALUE) { - // File is too large - } - - // Create the byte array to hold the data - byte[] bytes = new byte[(int) length]; - - // Read in the bytes - int offset = 0; - int numRead = 0; - while (offset < bytes.length && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) { - offset += numRead; - } - - // Ensure all the bytes have been read in - if (offset < bytes.length) { - throw new IOException("Could not completely read file " + file.getName()); - } - - // Close the input stream and return bytes - is.close(); - return bytes; - } -} diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPSerializerTest.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPSerializerTest.java index a3c84faa98..2146b3de60 100644 --- a/server/src/test/java/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPSerializerTest.java +++ b/server/src/test/java/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPSerializerTest.java @@ -1,14 +1,21 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2017 Mirth Corporation +// SPDX-FileCopyrightText: 2026 Mitch Gaffigan package com.mirth.connect.plugins.datatypes.ncpdp; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import java.io.File; + +import org.apache.commons.io.FileUtils; import org.junit.Before; import org.junit.Test; import org.xml.sax.SAXParseException; import com.mirth.connect.donkey.model.message.MessageSerializerException; +import com.mirth.connect.model.converters.TestUtil; import com.mirth.connect.model.datatype.SerializerProperties; public class NCPDPSerializerTest { @@ -41,6 +48,89 @@ public void testFromXMLWithExternalDtd() { assertTrue(exceptionCaught); } + @Test + public void test51RequestToXml() throws Exception { + String input = read("test-ncpdp-51-request-input.txt"); + String output = read("test-ncpdp-51-request-output.xml"); + assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); + } + + @Test + public void test51RequestFromXml() throws Exception { + String input = read("test-ncpdp-51-request-output.xml"); + String output = read("test-ncpdp-51-request-input.txt"); + assertEquals(output, serializer.fromXML(input)); + } + + @Test + public void test51ResponseToXml() throws Exception { + String input = read("test-ncpdp-51-response-input.txt"); + String output = read("test-ncpdp-51-response-output.xml"); + assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); + } + + @Test + public void test51ResponseFromXml() throws Exception { + String input = read("test-ncpdp-51-response-output.xml"); + String output = read("test-ncpdp-51-response-input.txt"); + assertEquals(output, serializer.fromXML(input)); + } + + @Test + public void testD0RequestToXml() throws Exception { + String input = read("test-ncpdp-d0-request-input.txt"); + String output = read("test-ncpdp-d0-request-output.xml"); + assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); + } + + @Test + public void testD0RequestFromXml() throws Exception { + String input = read("test-ncpdp-d0-request-output.xml"); + String output = read("test-ncpdp-d0-request-input.txt"); + assertEquals(output, serializer.fromXML(input)); + } + + @Test + public void testD0ResponseToXml() throws Exception { + String input = read("test-ncpdp-d0-response-input.txt"); + String output = read("test-ncpdp-d0-response-output.xml"); + assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); + } + + @Test + public void testD0ResponseFromXml() throws Exception { + String input = read("test-ncpdp-d0-response-output.xml"); + String output = read("test-ncpdp-d0-response-input.txt"); + assertEquals(output, serializer.fromXML(input)); + } + + @Test + public void testvalidateTransformHeaderWithfieldValue() throws Exception { + String input = "6100"; + String expectedOutput = "6100 "; + String actualOutput = serializer.validateTransformHeader(input); + assertEquals(actualOutput, expectedOutput); + } + + @Test + public void testvalidateTransformHeaderWithoutFieldValue() throws Exception { + String input = ""; + String expectedOutput = " "; + String actualOutput = serializer.validateTransformHeader(input); + assertEquals(actualOutput, expectedOutput); + } + + @Test + public void testvalidateTransformHeaderWithOneEndTag() throws Exception { + String input = ""; + String expectedOutput = " "; + String actualOutput = serializer.validateTransformHeader(input); + assertEquals(actualOutput, expectedOutput); + } + private static String read(String name) throws Exception { + return FileUtils.readFileToString(new File("tests/" + name)); + } + private static String validXml = "\r\n" + "bar"; diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPSerializerTests.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPSerializerTests.java deleted file mode 100644 index 99496d723a..0000000000 --- a/server/src/test/java/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPSerializerTests.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (c) Mirth Corporation. All rights reserved. - * - * http://www.mirthcorp.com - * - * The software in this package is published under the terms of the MPL license a copy of which has - * been included with this distribution in the LICENSE.txt file. - */ - -package com.mirth.connect.plugins.datatypes.ncpdp; - -import java.io.File; - -import junit.framework.Assert; - -import org.apache.commons.io.FileUtils; -import org.junit.Before; -import org.junit.Test; - -import com.mirth.connect.model.converters.TestUtil; - -public class NCPDPSerializerTests { - private NCPDPDataTypeProperties defaultProperties; - - @Before - public void setUp() throws Exception { - defaultProperties = new NCPDPDataTypeProperties(); -// defaultProperties = new Properties(); -// defaultProperties.put("segmentDelimiter", "0x1E"); -// defaultProperties.put("groupDelimiter", "0x1D"); -// defaultProperties.put("fieldDelimiter", "0x1C"); -// defaultProperties.put("useStrictValidation", "false"); - } - - @Test - public void test51RequestToXml() throws Exception { - String input = FileUtils.readFileToString(new File("tests/test-ncpdp-51-request-input.txt")); - String output = FileUtils.readFileToString(new File("tests/test-ncpdp-51-request-output.xml")); - NCPDPSerializer serializer = new NCPDPSerializer(defaultProperties.getSerializerProperties()); - Assert.assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); - } - - @Test - public void test51RequestFromXml() throws Exception { - String input = FileUtils.readFileToString(new File("tests/test-ncpdp-51-request-output.xml")); - String output = FileUtils.readFileToString(new File("tests/test-ncpdp-51-request-input.txt")); - NCPDPSerializer serializer = new NCPDPSerializer(defaultProperties.getSerializerProperties()); - Assert.assertEquals(output, serializer.fromXML(input)); - } - - @Test - public void test51ResponseToXml() throws Exception { - String input = FileUtils.readFileToString(new File("tests/test-ncpdp-51-response-input.txt")); - String output = FileUtils.readFileToString(new File("tests/test-ncpdp-51-response-output.xml")); - NCPDPSerializer serializer = new NCPDPSerializer(defaultProperties.getSerializerProperties()); - Assert.assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); - } - - @Test - public void test51ResponseFromXml() throws Exception { - String input = FileUtils.readFileToString(new File("tests/test-ncpdp-51-response-output.xml")); - String output = FileUtils.readFileToString(new File("tests/test-ncpdp-51-response-input.txt")); - NCPDPSerializer serializer = new NCPDPSerializer(defaultProperties.getSerializerProperties()); - Assert.assertEquals(output, serializer.fromXML(input)); - } - - @Test - public void testD0ToXml() throws Exception { - String input = FileUtils.readFileToString(new File("tests/test-ncpdp-d0-input.txt")); - String output = FileUtils.readFileToString(new File("tests/test-ncpdp-d0-output.xml")); - NCPDPSerializer serializer = new NCPDPSerializer(defaultProperties.getSerializerProperties()); - Assert.assertEquals(output, TestUtil.prettyPrintXml(serializer.toXML(input))); - } - - @Test - public void testD0FromXml() throws Exception { - String input = FileUtils.readFileToString(new File("tests/test-ncpdp-d0-output.xml")); - String output = FileUtils.readFileToString(new File("tests/test-ncpdp-d0-input.txt")); - NCPDPSerializer serializer = new NCPDPSerializer(defaultProperties.getSerializerProperties()); - Assert.assertEquals(output, serializer.fromXML(input)); - } - - @Test - public void testvalidateTransformHeaderWithfieldValue() throws Exception { - String input = "6100"; - String expectedOutput = "6100 "; - NCPDPSerializer serializer = new NCPDPSerializer(defaultProperties.getSerializerProperties()); - String actualOutput = serializer.validateTransformHeader(input); - Assert.assertEquals(actualOutput, expectedOutput); - } - - @Test - public void testvalidateTransformHeaderWithoutFieldValue() throws Exception { - String input = ""; - String expectedOutput = " "; - NCPDPSerializer serializer = new NCPDPSerializer(defaultProperties.getSerializerProperties()); - String actualOutput = serializer.validateTransformHeader(input); - Assert.assertEquals(actualOutput, expectedOutput); - } - - @Test - public void testvalidateTransformHeaderWithOneEndTag() throws Exception { - String input = ""; - String expectedOutput = " "; - NCPDPSerializer serializer = new NCPDPSerializer(defaultProperties.getSerializerProperties()); - String actualOutput = serializer.validateTransformHeader(input); - Assert.assertEquals(actualOutput, expectedOutput); - } -} diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPTests.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPTests.java deleted file mode 100644 index 3890a4dd57..0000000000 --- a/server/src/test/java/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPTests.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright (c) Mirth Corporation. All rights reserved. - * - * http://www.mirthcorp.com - * - * The software in this package is published under the terms of the MPL license a copy of which has - * been included with this distribution in the LICENSE.txt file. - */ - -package com.mirth.connect.plugins.datatypes.ncpdp; - -import java.io.File; -import java.io.IOException; -import java.io.StringReader; -import java.util.ArrayList; - -import org.apache.commons.io.FileUtils; -import org.w3c.dom.Document; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; -import org.xml.sax.XMLReader; -import org.xml.sax.helpers.XMLReaderFactory; - -import com.mirth.connect.donkey.model.message.MessageSerializerException; -import com.mirth.connect.model.converters.DocumentSerializer; -import com.mirth.connect.model.converters.Stopwatch; - -public class NCPDPTests { - public static void main(String[] args) throws Exception { - String testMessage = ""; - ArrayList testFiles = new ArrayList(); - testFiles.add("C:\\NCPDP_51_B1_Request.txt"); - testFiles.add("C:\\NCPDP_51_B1_Request_v2.txt"); - testFiles.add("C:\\NCPDP_51_B1_Response.txt"); - testFiles.add("C:\\NCPDP_51_B1_Response_v2.txt"); - testFiles.add("C:\\NCPDP_51_B1_Response_v3.txt"); - testFiles.add("C:\\NCPDP_51_B1_Response_v4.txt"); - testFiles.add("C:\\NCPDP_51_B1_Response_v5.txt"); - testFiles.add("C:\\NCPDP_51_B1_Response_v6.txt"); - testFiles.add("C:\\NCPDP_51_B1_Response_v7.txt"); - testFiles.add("C:\\NCPDP_51_B1_Response_v8.txt"); - testFiles.add("C:\\NCPDP_51_B1_Response_v9.txt"); - testFiles.add("C:\\NCPDP_51_B2_Request.txt"); - testFiles.add("C:\\NCPDP_51_B2_Request_v2.txt"); - testFiles.add("C:\\NCPDP_51_B2_Response.txt"); - testFiles.add("C:\\NCPDP_51_B2_Response_v2.txt"); - testFiles.add("C:\\NCPDP_51_B2_Response_v3.txt"); - testFiles.add("C:\\NCPDP_51_B3_Request.txt"); - testFiles.add("C:\\NCPDP_51_B3_Response.txt"); - testFiles.add("C:\\NCPDP_51_B3_Response_v2.txt"); - testFiles.add("C:\\NCPDP_51_B3_Response_v3.txt"); - testFiles.add("C:\\NCPDP_51_E1_Request.txt"); - testFiles.add("C:\\NCPDP_51_E1_Response.txt"); - testFiles.add("C:\\NCPDP_51_E1_Response_v2.txt"); - testFiles.add("C:\\NCPDP_51_E1_Response_v3.txt"); - testFiles.add("C:\\NCPDP_51_E1_Response_v4.txt"); - testFiles.add("C:\\NCPDP_51_E1_Response_v5.txt"); - testFiles.add("C:\\NCPDP_51_N1_Request.txt"); - testFiles.add("C:\\NCPDP_51_N2_Request.txt"); - testFiles.add("C:\\NCPDP_51_P1_Request.txt"); - testFiles.add("C:\\NCPDP_51_P1_Response.txt"); - testFiles.add("C:\\NCPDP_51_P1_Response_v2.txt"); - testFiles.add("C:\\NCPDP_51_P1_Response_v3.txt"); - testFiles.add("C:\\NCPDP_51_P2_Request.txt"); - testFiles.add("C:\\NCPDP_51_P2_Response.txt"); - testFiles.add("C:\\NCPDP_51_P3_Request.txt"); - testFiles.add("C:\\NCPDP_51_P3_Response.txt"); - testFiles.add("C:\\NCPDP_51_P3_Response_v2.txt"); - testFiles.add("C:\\NCPDP_51_P4_Request.txt"); - testFiles.add("C:\\NCPDP_51_P4_Response.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_1.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_2.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_3.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_4.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_5.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_6.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_7.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_8.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_9.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_10.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_11.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_12.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_13.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_14.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_15.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_16.txt"); - testFiles.add("C:\\NCPDP_51_CALPOS_17.txt"); - - for (String testFile : testFiles) { - testMessage = new String(FileUtils.readFileToByteArray(new File(testFile))); - System.out.println("Processing test file:" + testFile); - - try { - long totalExecutionTime = 0; - int iterations = 1; - for (int i = 0; i < iterations; i++) { - totalExecutionTime += runTest(testMessage); - } - - //System.out.println("Execution time average: " + totalExecutionTime/iterations + " ms"); - } - // System.out.println(new X12Serializer().serialize("SEG*1*2**4*5")); - catch (SAXException e) { - e.printStackTrace(); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - private static long runTest(String testMessage) throws MessageSerializerException, SAXException, IOException { - Stopwatch stopwatch = new Stopwatch(); -// Properties properties = new Properties(); - String SchemaUrl = "/ncpdp51.xsd"; -// properties.put("useStrictParser", "true"); -// properties.put("http://java.sun.com/xml/jaxp/properties/schemaSource",SchemaUrl); - stopwatch.start(); - NCPDPSerializer serializer = new NCPDPSerializer(null); - String xmloutput = serializer.toXML(testMessage); - //System.out.println(xmloutput); - DocumentSerializer docser = new DocumentSerializer(); - Document doc = docser.fromXML(xmloutput); - XMLReader xr = XMLReaderFactory.createXMLReader(); - - NCPDPXMLHandler handler = new NCPDPXMLHandler("\u001E", "\u001D", "\u001C", "51"); - - xr.setContentHandler(handler); - xr.setErrorHandler(handler); - xr.setFeature("http://xml.org/sax/features/validation", true); - xr.setFeature("http://apache.org/xml/features/validation/schema", true); - xr.setFeature("http://apache.org/xml/features/validation/schema-full-checking", true); - xr.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); - xr.setProperty("http://java.sun.com/xml/jaxp/properties/schemaLanguage", "http://www.w3.org/2001/XMLSchema"); - xr.setProperty("http://apache.org/xml/properties/schema/external-noNamespaceSchemaLocation", SchemaUrl); - xr.setProperty("http://java.sun.com/xml/jaxp/properties/schemaSource", "/ncpdp51.xsd"); - xr.parse(new InputSource(new StringReader(xmloutput))); - stopwatch.stop(); - - //System.out.println(docser.serialize(doc)); //handler.getOutput()); - //System.out.println(handler.getOutput()); - //System.out.println(xmloutput); - if (handler.getOutput().toString().replace('\n', '\r').trim().equals(testMessage.replaceAll("\\r\\n", "\r").trim())) { - System.out.println("Test Successful!"); - } else { - String original = testMessage.replaceAll("\\r\\n", "\r").trim(); - String newm = handler.getOutput().toString().replace('\n', '\r').trim(); - for (int i = 0; i < original.length(); i++) { - if (original.charAt(i) == newm.charAt(i)) { - System.out.print(newm.charAt(i)); - } else { - System.out.println(""); - System.out.print("Saw: "); - System.out.println(newm.charAt(i)); - System.out.print("Expected: "); - System.out.print(original.charAt(i)); - break; - } - } - System.out.println("Test Failed!"); - } - return stopwatch.toValue(); - } -} diff --git a/server/src/test/java/com/mirth/connect/server/util/PasswordRequirementsTests.java b/server/src/test/java/com/mirth/connect/server/util/PasswordRequirementsTest.java similarity index 97% rename from server/src/test/java/com/mirth/connect/server/util/PasswordRequirementsTests.java rename to server/src/test/java/com/mirth/connect/server/util/PasswordRequirementsTest.java index e21b4a683e..66494fdd96 100644 --- a/server/src/test/java/com/mirth/connect/server/util/PasswordRequirementsTests.java +++ b/server/src/test/java/com/mirth/connect/server/util/PasswordRequirementsTest.java @@ -14,7 +14,7 @@ import com.mirth.connect.client.core.ControllerException; import com.mirth.connect.model.PasswordRequirements; -public class PasswordRequirementsTests extends TestCase { +public class PasswordRequirementsTest extends TestCase { protected void setUp() throws Exception { super.setUp(); diff --git a/server/src/test/java/com/mirth/connect/server/util/Pre22PasswordCheckerTests.java b/server/src/test/java/com/mirth/connect/server/util/Pre22PasswordCheckerTest.java similarity index 92% rename from server/src/test/java/com/mirth/connect/server/util/Pre22PasswordCheckerTests.java rename to server/src/test/java/com/mirth/connect/server/util/Pre22PasswordCheckerTest.java index 0d125317e8..90c2434941 100644 --- a/server/src/test/java/com/mirth/connect/server/util/Pre22PasswordCheckerTests.java +++ b/server/src/test/java/com/mirth/connect/server/util/Pre22PasswordCheckerTest.java @@ -13,7 +13,7 @@ import org.junit.Test; -public class Pre22PasswordCheckerTests { +public class Pre22PasswordCheckerTest { @Test public void testCheckPassword() throws Exception { diff --git a/server/src/test/java/com/mirth/connect/server/util/TemplateValueReplacerTests.java b/server/src/test/java/com/mirth/connect/server/util/TemplateValueReplacerTest.java similarity index 98% rename from server/src/test/java/com/mirth/connect/server/util/TemplateValueReplacerTests.java rename to server/src/test/java/com/mirth/connect/server/util/TemplateValueReplacerTest.java index d763a136e8..3c45cb7f52 100644 --- a/server/src/test/java/com/mirth/connect/server/util/TemplateValueReplacerTests.java +++ b/server/src/test/java/com/mirth/connect/server/util/TemplateValueReplacerTest.java @@ -19,7 +19,7 @@ import org.junit.Before; import org.junit.Test; -public class TemplateValueReplacerTests { +public class TemplateValueReplacerTest { private TemplateValueReplacer templateReplacer; diff --git a/server/src/test/java/com/mirth/connect/util/ValueReplacerTests.java b/server/src/test/java/com/mirth/connect/util/ValueReplacerTest.java similarity index 99% rename from server/src/test/java/com/mirth/connect/util/ValueReplacerTests.java rename to server/src/test/java/com/mirth/connect/util/ValueReplacerTest.java index 2acfadbbf6..1d7a79dc75 100644 --- a/server/src/test/java/com/mirth/connect/util/ValueReplacerTests.java +++ b/server/src/test/java/com/mirth/connect/util/ValueReplacerTest.java @@ -26,7 +26,7 @@ import com.mirth.connect.donkey.model.message.ConnectorMessage; -public class ValueReplacerTests { +public class ValueReplacerTest { private ValueReplacer replacer; private Map> map; private ConnectorMessage connectorMessage; diff --git a/server/tests/dicom/README.md b/server/tests/dicom/README.md new file mode 100644 index 0000000000..b135d29ac3 --- /dev/null +++ b/server/tests/dicom/README.md @@ -0,0 +1,22 @@ +# DICOM test samples + +All CC-BY licensed (commercial reuse OK with attribution), sourced from NCI Imaging Data Commons, +by way of https://saga-it.com/dicom/samples + +Each `.dcm` is paired with `.xml`, the expected serialization of its header. +`DICOMSerializerTest` discovers the pairs by scanning this directory, so a new sample needs no code +change. The XML covers the header only - `DICOMSerializer.removePixelData` strips pixel data before +serializing, since including it would dwarf the rest of the document. + +| Sample | Modality | Collection | +| --- | --- | --- | +| `ct-abdomen-c4kc-kits-instance` | CT | C4KC-KiTS | +| `ct-chest-lidc-idri-instance` | CT | LIDC-IDRI | +| `ct-lung-screening-nlst-instance` | CT | NLST | +| `mg-mammography-cbis-ddsm-instance` | MG | CBIS-DDSM | +| `mr-prostate-prostatex-instance` | MR | PROSTATEx | +| `pt-breast-qin-01-instance` | PT | QIN-BREAST | +| `us-lymph-node-cmb-lca-instance` | US | CMB-LCA | + +The patient identifiers in these files are the collections' own pseudonyms (for example +`LIDC-IDRI-0580`), not real ones. diff --git a/server/tests/dicom/ct-abdomen-c4kc-kits-instance.dcm b/server/tests/dicom/ct-abdomen-c4kc-kits-instance.dcm new file mode 100644 index 0000000000..ad88cdc16f Binary files /dev/null and b/server/tests/dicom/ct-abdomen-c4kc-kits-instance.dcm differ diff --git a/server/tests/dicom/ct-abdomen-c4kc-kits-instance.xml b/server/tests/dicom/ct-abdomen-c4kc-kits-instance.xml new file mode 100644 index 0000000000..98b7c67da5 --- /dev/null +++ b/server/tests/dicom/ct-abdomen-c4kc-kits-instance.xml @@ -0,0 +1,213 @@ + + + 206 + 00\01 + 1.2.840.10008.5.1.4.1.1.2 + 1.3.6.1.4.1.14519.5.2.1.6919.4624.157171626006094822772795196788 + 1.2.840.10008.1.2.1 + 1.3.6.1.4.1.22213.1.143 + 0.5 + POSDA + ISO_IR 100 + ORIGINAL\PRIMARY\AXIAL\CT_SOM5 SPI + 1.2.840.10008.5.1.4.1.1.2 + 1.3.6.1.4.1.14519.5.2.1.6919.4624.157171626006094822772795196788 + 20040912 + 20040912 + 20040912 + 20040912 + 20040912062713.470000 + 062339.242000 + 062946.132000 + 062713.470000 + 062713.470000 + + CT + SIEMENS + + chest_abdomen_pelvis__w + + + IMG2002 + FHS + CTA ANGIOGRAM CHEST/ABD/PELVIS W PROCESSING + + + arterial + SOMATOM Definition Edge + + + 1.2.840.10008.3.1.2.3.1 + 1.3.6.1.4.1.14519.5.2.1.6919.4624.179475577869989445825322252923 + + + + + 1.2.840.10008.5.1.4.1.1.2 + 1.3.6.1.4.1.14519.5.2.1.6919.4624.297042649168128363219752116592 + + + + + 1.3.12.2.1107.5.9.1 + 1.3.6.1.4.1.14519.5.2.1.6919.4624.688401930884469382984667769886 + + + 1.3.6.1.4.1.14519.5.2.1.6919.4624.688401930884469382984667769886 + SIEMENS CT VA1 DUMMY + KiTS-00136 + KiTS-00136 + + M + 052Y + 4 + -49 + Days offset from surgery + -49.0 + SURGERY + YES + Per DICOM PS 3.15 AnnexE. Details in 0012,0064 + + + 113100 + DCM + Basic Application Confidentiality Profile + + + 113101 + DCM + Clean Pixel Data Option + + + 113104 + DCM + Clean Structured Content Option + + + 113105 + DCM + Clean Descriptors Option + + + 113107 + DCM + Retain Longitudinal Temporal Information Modified Dates Option + + + 113108 + DCM + Retain Patient Characteristics Option + + + 113109 + DCM + Retain Device Identity Option + + + 113111 + DCM + Retain Safe Private Option + + + CTP + C4KC-KiTS + 69194624 + None + ABDOMEN + 5 + 100 + 500 + 0 + syngo CT VA48A + PE_CAP + 0 + 062615.760996 + 062615.760996 + 0 + 0 + 0 + 0 + 350 + 1085.6 + 595 + 0 + 151 + CW + 500 + 264 + 220 + FLAT + 30 + 1.2 + 20040912 + + I40f\2 + FFS + 0.6 + 38.4 + 46.0 + 23.0 + 0.6 + 0.0\-151.0\-504.3 + -5.0\-151.0\-504.3 + XYZ_EC + 53.0769 + 8.67425947826087 + + + 113691 + DCM + IEC Body Dosimetry Phantom + + + 0.679\0.714\0.746 + SIEMENS CT VA0 COAD + 0 + 0.9236 + -1.6565 + 0 + 23 + 1.3.6.1.4.1.14519.5.2.1.6919.4624.179475577869989445825322252923 + 1.3.6.1.4.1.14519.5.2.1.6919.4624.927472659782721902556191020020 + + 8 + 7 + 51 + -179.658203125\-325.658203125\-504.3 + 1\0\0\0\1\0 + 1.3.6.1.4.1.14519.5.2.1.6919.4624.240499823560899143234666603682 + + 504.3 + + SIEMENS MED + 5\0 + 1 + MONOCHROME2 + 512 + 512 + 0.68359375\0.68359375 + 16 + 12 + 11 + 0 + 0 + 2419 + MODIFIED + 60\-600 + 375\1200 + -1024 + 1 + HU + WINDOW1\WINDOW2 + SIEMENS CSA HEADER + SIEMENS MEDCOM HEADER + CTA ANGIOGRAM CHEST/ABD/PELVIS W PROCESSING + + + IMG2002 + FHS + CTA ANGIOGRAM CHEST/ABD/PELVIS W PROCESSING + + + STENTOR + diff --git a/server/tests/dicom/ct-chest-lidc-idri-instance.dcm b/server/tests/dicom/ct-chest-lidc-idri-instance.dcm new file mode 100644 index 0000000000..84ad9adfb6 Binary files /dev/null and b/server/tests/dicom/ct-chest-lidc-idri-instance.dcm differ diff --git a/server/tests/dicom/ct-chest-lidc-idri-instance.xml b/server/tests/dicom/ct-chest-lidc-idri-instance.xml new file mode 100644 index 0000000000..59f5e74dfb --- /dev/null +++ b/server/tests/dicom/ct-chest-lidc-idri-instance.xml @@ -0,0 +1,115 @@ + + + 204 + 00\01 + 1.2.840.10008.5.1.4.1.1.2 + 1.3.6.1.4.1.14519.5.2.1.6279.6001.147549101770122056857403430614 + 1.2.840.10008.1.2 + 1.3.6.1.4.1.22213.1.143 + 0.5 + POSDA + ISO_IR 100 + ORIGINAL\PRIMARY\AXIAL + + + 1.2.840.10008.5.1.4.1.1.2 + 1.3.6.1.4.1.14519.5.2.1.6279.6001.147549101770122056857403430614 + 20000101 + 20000101 + 20000101 + 20000101 + 20000101 + 20000101 + 20000101 + + + + + 0 + IMA SCAN + + CT + GE MEDICAL SYSTEMS + + LightSpeed Ultra + + LIDC-IDRI-0580 + + + + 20000101 + YES + DCM:113100/113105/113107/113108/113109/113111 + CTP + LIDC-IDRI + 62796001 + CHEST + RM + HELICAL MODE + 1.250000 + 120 + 0.500000 + 500.000000 + LightSpeedApps304.3_H3.1M3 + 360.000000 + 949.075012 + 541.000000 + 0.000000 + 156.500000 + 504 + 80 + 960 + BODY FILTER + 10 + 0.700000 + STANDARD + 5.787642 + FFS + 1.3.6.1.4.1.14519.5.2.1.6279.6001.173480979711457247360986415860 + 1.3.6.1.4.1.14519.5.2.1.6279.6001.237215747217294006286437405216 + + 30811 + 1 + 126 + 180\-180\0 + -184.600006\-174.800003\-127.230003 + -1\0\0\0\1\0 + 1.000000\0.000000\0.000000\0.000000\1.000000\0.000000 + -127.23 + 1.3.6.1.4.1.14519.5.2.1.6279.6001.269849279208588310379687197292 + PLANAR + SN + -127.230003 + 1 + MONOCHROME2 + 2 + 512 + 512 + 007.031250e-01\007.031250e-01 + RECT + NONE + 16 + 16 + 15 + 1 + 63536 + 32736 + MODIFIED + 50 + 500 + -1024 + 1 + + 20000101 + 20000101 + 20000101 + 20000101 + + + + + + Removed by CTP + Removed by CTP + + diff --git a/server/tests/dicom/ct-lung-screening-nlst-instance.dcm b/server/tests/dicom/ct-lung-screening-nlst-instance.dcm new file mode 100644 index 0000000000..e5e2d1d1ab Binary files /dev/null and b/server/tests/dicom/ct-lung-screening-nlst-instance.dcm differ diff --git a/server/tests/dicom/ct-lung-screening-nlst-instance.xml b/server/tests/dicom/ct-lung-screening-nlst-instance.xml new file mode 100644 index 0000000000..302c24f405 --- /dev/null +++ b/server/tests/dicom/ct-lung-screening-nlst-instance.xml @@ -0,0 +1,118 @@ + + + 190 + 00\01 + 1.2.840.10008.5.1.4.1.1.2 + 1.2.840.113654.2.55.12882863977514288242837811272669865908 + 1.2.840.10008.1.2.1 + 1.2.40.0.13.1.1.1 + dcm4che-1.4.31 + ISO_IR 100 + ORIGINAL\PRIMARY\AXIAL\CT_SOM5 SPI + 1.2.840.10008.5.1.4.1.1.2 + 1.2.840.113654.2.55.12882863977514288242837811272669865908 + 19990102 + + 888953 + CT + SIEMENS + + S13 + NLST-LSS + 0,OPA,SE,VZOOM,B30f,278,2,120,60,30,na + Volume Zoom + 061226^LSS + 112516 + + + 061226 + National Cancer Institute + NCT00047385 + NLST-LSS + + + 061226 + T0 + Washington U (images), Westat (assoc. data) + YES + 113100\113101\113103\113105\113109 + + + 113100 + DCM + Basic Application Confidentiality Profile + + + 113101 + DCM + Clean Pixel Data Option + + + 113103 + DCM + Clean Graphics Option + + + 113105 + DCM + Clean Descriptors Option + + + 113109 + DCM + Retain Device Identity Option + + + CTP + NLST + NLST + LSS\00 + 70049004 + CHEST + 2 + 120 + VA40C + 278 + 1040 + 570 + 0 + 145 + CW + 500 + 120 + 30 + 0 + 14 + 1.2 + B30f + 1.2.840.113654.2.55.33575893932308185246496913106863435791 + 1.2.840.113654.2.55.57669092321627294898588883865157458147 + + 6 + 3 + 107 + + -138.72852\-260.72852\-337.4 + 1\0\0\0\1\0 + 1.2.840.113654.2.55.85903792958366369381906191157480138431 + + 337.4 + 1 + MONOCHROME2 + 2 + 512 + 512 + 005.429688e-01\005.429688e-01 + RECT + NONE + 16 + 12 + 11 + 0 + 32736 + 35\400 + 400\3000 + -1024 + 1 + WINDOW1\WINDOW2 + diff --git a/server/tests/dicom/mg-mammography-cbis-ddsm-instance.dcm b/server/tests/dicom/mg-mammography-cbis-ddsm-instance.dcm new file mode 100644 index 0000000000..dfd7bb1a52 Binary files /dev/null and b/server/tests/dicom/mg-mammography-cbis-ddsm-instance.dcm differ diff --git a/server/tests/dicom/mg-mammography-cbis-ddsm-instance.xml b/server/tests/dicom/mg-mammography-cbis-ddsm-instance.xml new file mode 100644 index 0000000000..c87ece87e7 --- /dev/null +++ b/server/tests/dicom/mg-mammography-cbis-ddsm-instance.xml @@ -0,0 +1,49 @@ + + + 194 + 00\01 + 1.2.840.10008.5.1.4.1.1.7 + 1.3.6.1.4.1.9590.100.1.2.145141605511468979636497478400886398073 + 1.2.840.10008.1.2 + 1.2.40.0.13.1.1.1 + dcm4che-1.4.35 + ISO_IR 100 + 1.2.840.10008.5.1.4.1.1.7 + 1.3.6.1.4.1.9590.100.1.2.145141605511468979636497478400886398073 + 20161004 + 20160426 + 152851 + 135218.377000 + + MG + WSD + + cropped images + Mass-Test_P_01510_RIGHT_CC_1 + Mass-Test_P_01510_RIGHT_CC_1 + + + CTP + CBIS-DDSM + 43372602 + BREAST + MathWorks + MATLAB + 1.3.6.1.4.1.9590.100.1.2.267826310912478261209862427462784808028 + 1.3.6.1.4.1.9590.100.1.2.88158044112350000736236683583018260323 + DDSM + 1 + 1 + CC + R + 1 + MONOCHROME2 + 386 + 386 + 16 + 16 + 15 + 0 + 38064 + 65535 + diff --git a/server/tests/dicom/mr-prostate-prostatex-instance.dcm b/server/tests/dicom/mr-prostate-prostatex-instance.dcm new file mode 100644 index 0000000000..9d27edbdb5 Binary files /dev/null and b/server/tests/dicom/mr-prostate-prostatex-instance.dcm differ diff --git a/server/tests/dicom/mr-prostate-prostatex-instance.xml b/server/tests/dicom/mr-prostate-prostatex-instance.xml new file mode 100644 index 0000000000..a89f5ad7d4 --- /dev/null +++ b/server/tests/dicom/mr-prostate-prostatex-instance.xml @@ -0,0 +1,181 @@ + + + 196 + 00\01 + 1.2.840.10008.5.1.4.1.1.4 + 1.3.6.1.4.1.14519.5.2.1.7311.5101.174080670823095725391876950759 + 1.2.840.10008.1.2.1 + 1.2.40.0.13.1.1.1 + dcm4che-1.4.35 + ISO_IR 100 + DERIVED\PRIMARY\DIFFUSION\TRACEW\DIS2D + 20120222 + 093124.703000 + 1.2.840.10008.5.1.4.1.1.4 + 1.3.6.1.4.1.14519.5.2.1.7311.5101.174080670823095725391876950759 + 20120222 + 20120222 + 20120222 + 20120222 + 091218.078000 + 093124.609000 + 093113.347500 + 093124.703000 + 8913853518574410 + MR + SIEMENS + MR prostaat kanker detectie_mc MCAPRODET + ep2d_diff_tra_DYNDIST + Skyra + + + 1.2.840.10008.3.1.2.3.1 + 1.3.6.1.4.1.14519.5.2.1.7311.5101.989962386484007264256688307298 + + + ProstateX-0204 + ProstateX-0204 + + M + 068Y + 1.85 + 78 + YES + DIAG-ANON 0.3\Per DICOM PS 3.15 AnnexE. Details in 0012,0064 + + + 113100 + DCM + Basic Application Confidentiality Profile + + + 113101 + DCM + Clean Pixel Data Option + + + 113104 + DCM + Clean Structured Content Option + + + 113105 + DCM + Clean Descriptors Option + + + 113107 + DCM + Retain Longitudinal Temporal Information Modified Dates Option + + + 113108 + DCM + Retain Patient Characteristics Option + + + 113109 + DCM + Retain Device Identity Option + + + 113111 + DCM + Retain Safe Private Option + + + CTP + PROSTATEx + 73105101 + PROSTATE + EP + SK\SP\OSP + PFP\FS + 2D + *ep_b400t + N + 3 + 2700 + 63 + 8 + 123.240954 + 1H + 1 + 3 + 3 + 95 + 48 + 100 + 65.625 + 1500 + syngo MR D11 + ep2d_diff_tra + Body + 0\128\84\0 + ROW + 90 + N + 0.57373217280629 + 0 + FFS + SIEMENS MR HEADER + 33\35 + 34\30\30\20 + 49\53\4F\54\52\4F\50\49\43\20 + 46\61\73\74 + 4E\6F + 00\00\00\00\00\00\00\00\6E\F8\FF\FF + 00\00\00\00\00\00\00\00\73\F8\FF\FF + 30\5C\30\5C\2D\35 + DC\2C\1C\10\7A\C9\52\C0\22\8E\33\55\09\EE\59\C0\75\95\8B\84\78\4C\49\40 + 31\39\33\2E\36\37\35\20 + 31\20 + 32\36\30\30 + 77\BE\9F\1A\2F\9D\34\40 + 1.3.6.1.4.1.14519.5.2.1.7311.5101.989962386484007264256688307298 + 1.3.6.1.4.1.14519.5.2.1.7311.5101.184815022142880568898393767017 + + 6 + 1 + 29 + -75.148075130013\-103.71931963112\45.597427910633 + 1\-1.885178e-010\8.07988e-011\2.051034e-010\0.91913534192834\-0.3939419033541 + 1.3.6.1.4.1.14519.5.2.1.7311.5101.221351907157888248216888359615 + + 1.0508213036143 + 1 + MONOCHROME2 + 128 + 84 + 2\2 + 16 + 12 + 11 + 0 + 0 + 320 + MODIFIED + 89 + 239 + Algo1 + SIEMENS CSA HEADER + SIEMENS MEDCOM HEADER2 + 63\6F\6D\20 + MITRA LINKED ATTRIBUTES 1.0 + AGFA PACS Archive Mirroring 1.0 + ARRIVED + MED + MR prostaat kanker detectie_mc MCAPRODET + MR prostaat kanker detectie_mc + MITRA OBJECT UTF8 ATTRIBUTES 1.0 + 20120222 + 091218.156000 + MR prostaat kanker detectie_mc + + SIEMENS MR HEADER + 49\4D\41\47\45\20\4E\55\4D\20\34\20 + 31\2E\30\20 + 38\34\70\2A\31\32\38\73 + 42\4F\31\2C\32\3B\53\50\35\2C\36\20 + 2B\4C\50\48 + diff --git a/server/tests/dicom/pt-breast-qin-01-instance.dcm b/server/tests/dicom/pt-breast-qin-01-instance.dcm new file mode 100644 index 0000000000..567b5ada7e Binary files /dev/null and b/server/tests/dicom/pt-breast-qin-01-instance.dcm differ diff --git a/server/tests/dicom/pt-breast-qin-01-instance.xml b/server/tests/dicom/pt-breast-qin-01-instance.xml new file mode 100644 index 0000000000..3272ec9351 --- /dev/null +++ b/server/tests/dicom/pt-breast-qin-01-instance.xml @@ -0,0 +1,169 @@ + + + 198 + 00\01 + 1.2.840.10008.5.1.4.1.1.128 + 1.3.6.1.4.1.14519.5.2.1.8162.7003.246092060369879550027417906384 + 1.2.840.10008.1.2.1 + 1.2.40.0.13.1.1.1 + dcm4che-1.4.31 + ISO_IR 100 + ORIGINAL\PRIMARY + 19930102 + 190248 + 1.3.6.1.4.1.14519.5.2.1.8162.7003.335605164322770524664245774978 + 1.2.840.10008.5.1.4.1.1.128 + 1.3.6.1.4.1.14519.5.2.1.8162.7003.246092060369879550027417906384 + 19930102 + 19930102 + 19930102 + 19930102 + 185346.00 + 185833.00 + 185833.00 + 190248 + 3195263453823563 + PT + GE MEDICAL SYSTEMS + + vumcct + BREAST PRONE + PET AC 3DWB + Discovery STE + QIN-BREAST-01-0020 + QIN-BREAST-01-0020 + + F + 045Y + 1.6 + 65 + + + YES + DCM:113100/113105/113107/113108/113109/113111 + CTP + QIN-BREAST + 81627003 + BREAST + 3.27 + TIME + MANU + 0 + 0 + 41.04 + PRONE BREAST + 0 + 0 + 0 + 0 + 700 + 0 + CYLINDRICAL RING + 700\153 + NONE + 120000 + HFP + 1.3.6.1.4.1.14519.5.2.1.8162.7003.301759690586071225280046587327 + 1.3.6.1.4.1.14519.5.2.1.8162.7003.222925049784905277500431157002 + + 3 + 56 + 347.265625\347.265625\-263.80999755859 + -1\0\0\0\-1\0 + 1.3.6.1.4.1.14519.5.2.1.8162.7003.146772878946225576316847306304 + V + -263.81 + 1 + MONOCHROME2 + 128 + 128 + 5.46875\5.46875 + DECY\ATTN\SCAT\DTIM\RAN\DCAL\SLSENS\NORM + 16 + 16 + 15 + 1 + 0 + 32767 + MODIFIED + 0 + 0.413549 + 00 + + + 425 + 650 + + + + + FDG -- fluorodeoxyglucose + 0 + 175700.00 + 175800.00 + 429823712 + 6588 + 0.97000002861023 + 19930102175700.00 + 19930102175800.00 + + + C-111A1 + SNM3 + 18F + + + + + C-B1031 + SNM3 + FDG -- fluorodeoxyglucose + + + + + 83 + NONE + + + F-10450 + SNM3 + recumbent + + + + + F-10310 + SNM3 + prone + + + + + F-10470 + SNM3 + headfirst + + + STATIC\IMAGE + BQML + EMISSION + SING + measured,, 0.096000 cm-1, + START + 3D IR + 0 + Model Based + 1\2 + 2 + 0 + 0 + 0 + 0 + 1 + 1.00633 + 1523 + 0.366749 + 1.30417 + 28 + diff --git a/server/tests/dicom/us-lymph-node-cmb-lca-instance.dcm b/server/tests/dicom/us-lymph-node-cmb-lca-instance.dcm new file mode 100644 index 0000000000..f155d50239 Binary files /dev/null and b/server/tests/dicom/us-lymph-node-cmb-lca-instance.dcm differ diff --git a/server/tests/dicom/us-lymph-node-cmb-lca-instance.xml b/server/tests/dicom/us-lymph-node-cmb-lca-instance.xml new file mode 100644 index 0000000000..186782c02d --- /dev/null +++ b/server/tests/dicom/us-lymph-node-cmb-lca-instance.xml @@ -0,0 +1,148 @@ + + + 208 + 00\01 + 1.2.840.10008.5.1.4.1.1.6.1 + 1.3.6.1.4.1.14519.5.2.1.185577958846157191089023023733319573972 + 1.2.840.10008.1.2.1 + 1.3.6.1.4.1.22213.1.143 + 0.5 + POSDA + ISO_IR 192 + DERIVED\PRIMARY\SMALL PARTS + 19591020 + 113433 + 1.2.840.10008.5.1.4.1.1.6.1 + 1.3.6.1.4.1.14519.5.2.1.185577958846157191089023023733319573972 + 19591020 + 19591020 + 19591020 + 19591020113433 + 100000 + 110358 + 113433 + + US + FOR PRESENTATION + Philips Medical Systems + + US_Biopsy_RCervicalNode + + + IMG1101 + GEIIS + 0 + US GUIDED THYROID FNA + + + US GUIDED LYMPH NODE SUPERFICIAL BIOPSY + EPIQ 5G + + + 1.2.840.100008.3.1.2.3.1 + 1.3.6.1.4.1.14519.5.2.1.207522269430030124341950649812029134159 + + + + + 1.2.840.10008.3.1.2.3.3 + 1.3.6.1.4.1.14519.5.2.1.241721883221006165848038945169821191225 + + + + MSB-02120 + MSB-02120 + + O + + 4 + -72.0 + REGISTRATION + YES + Per DICOM PS 3.15 AnnexE. Details in 0012,0064 + + + 113100 + DCM + Basic Application Confidentiality Profile + + + 113101 + DCM + Clean Pixel Data Option + + + 113104 + DCM + Clean Structured Content Option + + + 113105 + DCM + Clean Descriptors Option + + + 113107 + DCM + Retain Longitudinal Temporal Information Modified Dates Option + + + 113108 + DCM + Retain Patient Characteristics Option + + + 113109 + DCM + Retain Device Identity Option + + + 113111 + DCM + Retain Safe Private Option + + + CTP + CMB-LCA + 16001202 + EPIQ 5G_7.0.5.962 + 0 + L15_7io\54108 + SM_PRTS_SUPERFICIAL + + + 1 + 1 + 2 + 14 + 38 + 1010 + 758 + 3 + 3 + 0.003708797278190173 + 0.003708797278190173 + + + 1.3.6.1.4.1.14519.5.2.1.207522269430030124341950649812029134159 + 1.3.6.1.4.1.14519.5.2.1.58866608043609699358076981026977397142 + + 1 + 10 + 1 + MONOCHROME2 + 768 + 1024 + 0 + 8 + 8 + 7 + 0 + NO + MODIFIED + 127 + 254 + 00 + + IDENTITY + diff --git a/server/tests/test-1597-input-missing-elements.xml b/server/tests/test-1597-input-missing-elements.xml new file mode 100644 index 0000000000..b5789444eb --- /dev/null +++ b/server/tests/test-1597-input-missing-elements.xml @@ -0,0 +1,261 @@ + + + + 00 + + + + + + 00 + + + + + + ZZ + + + SUBMITTERID + + + ZZ + + + PAYERID + + + 260330 + + + 1430 + + + ^ + + + 00501 + + + 000000001 + + + 0 + + + T + + + + + + + + + HS + + + SUBMITTERID + + + PAYERID + + + 20260330 + + + 1430 + + + 1 + + + X + + + 005010X279A1 + + + + + 270 + + + 0001 + + + 000010001 + + + + + 0022 + + + 13 + + + REF123456789 + + + 20260330 + + + 1430 + + + + + 1 + + + + 20 + + + 1 + + + + + PR + + + 2 + + + GREAT HEALTH PLAN + + + PI + + + PAYER12345 + + + + + 2 + + + 1 + + + 21 + + + 1 + + + + + 1P + + + 2 + + + COMMUNITY HOSPITAL + + + + + + + XX + + + 1234567890 + + + + + 3 + + + 2 + + + 22 + + + 0 + + + + + 1 + + + 987654321 + + + 1234567890 + + + + + IL + + + 1 + + + SMITH + + + JOHN + + + M + + + + + MI + + + SUB123456789 + + + + + D8 + + + 19800101 + + + M + + + + + 30 + + + + + 13 + + + 0001 + + + + + 1 + + + 1 + + + + + 1 + + + 000000001 + + + diff --git a/server/tests/test-1597-input.xml b/server/tests/test-1597-input.xml new file mode 100644 index 0000000000..0a7ded8aeb --- /dev/null +++ b/server/tests/test-1597-input.xml @@ -0,0 +1,265 @@ + + + + 00 + + + + + + 00 + + + + + + ZZ + + + SUBMITTERID + + + ZZ + + + PAYERID + + + 260330 + + + 1430 + + + ^ + + + 00501 + + + 000000001 + + + 0 + + + T + + + + + + + + + HS + + + SUBMITTERID + + + PAYERID + + + 20260330 + + + 1430 + + + 1 + + + X + + + 005010X279A1 + + + + + 270 + + + 0001 + + + 000010001 + + + + + 0022 + + + 13 + + + REF123456789 + + + 20260330 + + + 1430 + + + + + 1 + + + + 20 + + + 1 + + + + + PR + + + 2 + + + GREAT HEALTH PLAN + + + + + + + PI + + + PAYER12345 + + + + + 2 + + + 1 + + + 21 + + + 1 + + + + + 1P + + + 2 + + + COMMUNITY HOSPITAL + + + + + + + XX + + + 1234567890 + + + + + 3 + + + 2 + + + 22 + + + 0 + + + + + 1 + + + 987654321 + + + 1234567890 + + + + + IL + + + 1 + + + SMITH + + + JOHN + + + M + + + + + MI + + + SUB123456789 + + + + + D8 + + + 19800101 + + + M + + + + + 30 + + + + + 13 + + + 0001 + + + + + 1 + + + 1 + + + + + 1 + + + 000000001 + + + diff --git a/server/tests/test-1597-output.txt b/server/tests/test-1597-output.txt new file mode 100644 index 0000000000..e1a30e694e --- /dev/null +++ b/server/tests/test-1597-output.txt @@ -0,0 +1,16 @@ +ISA*00* *00* *ZZ*SUBMITTERID *ZZ*PAYERID *260330*1430*^*00501*000000001*0*T*:~ +GS*HS*SUBMITTERID*PAYERID*20260330*1430*1*X*005010X279A1~ +ST*270*0001*000010001~ +BHT*0022*13*REF123456789*20260330*1430~ +HL*1**20*1~ +NM1*PR*2*GREAT HEALTH PLAN*****PI*PAYER12345~ +HL*2*1*21*1~ +NM1*1P*2*COMMUNITY HOSPITAL*****XX*1234567890~ +HL*3*2*22*0~ +TRN*1*987654321*1234567890~ +NM1*IL*1*SMITH*JOHN*M***MI*SUB123456789~ +DMG*D8*19800101*M~ +EQ*30~ +SE*13*0001~ +GE*1*1~ +IEA*1*000000001~ diff --git a/server/tests/test-edi-input.txt b/server/tests/test-edi-input.txt new file mode 100644 index 0000000000..e1a30e694e --- /dev/null +++ b/server/tests/test-edi-input.txt @@ -0,0 +1,16 @@ +ISA*00* *00* *ZZ*SUBMITTERID *ZZ*PAYERID *260330*1430*^*00501*000000001*0*T*:~ +GS*HS*SUBMITTERID*PAYERID*20260330*1430*1*X*005010X279A1~ +ST*270*0001*000010001~ +BHT*0022*13*REF123456789*20260330*1430~ +HL*1**20*1~ +NM1*PR*2*GREAT HEALTH PLAN*****PI*PAYER12345~ +HL*2*1*21*1~ +NM1*1P*2*COMMUNITY HOSPITAL*****XX*1234567890~ +HL*3*2*22*0~ +TRN*1*987654321*1234567890~ +NM1*IL*1*SMITH*JOHN*M***MI*SUB123456789~ +DMG*D8*19800101*M~ +EQ*30~ +SE*13*0001~ +GE*1*1~ +IEA*1*000000001~ diff --git a/server/tests/test-edi-output.xml b/server/tests/test-edi-output.xml new file mode 100644 index 0000000000..0a7ded8aeb --- /dev/null +++ b/server/tests/test-edi-output.xml @@ -0,0 +1,265 @@ + + + + 00 + + + + + + 00 + + + + + + ZZ + + + SUBMITTERID + + + ZZ + + + PAYERID + + + 260330 + + + 1430 + + + ^ + + + 00501 + + + 000000001 + + + 0 + + + T + + + + + + + + + HS + + + SUBMITTERID + + + PAYERID + + + 20260330 + + + 1430 + + + 1 + + + X + + + 005010X279A1 + + + + + 270 + + + 0001 + + + 000010001 + + + + + 0022 + + + 13 + + + REF123456789 + + + 20260330 + + + 1430 + + + + + 1 + + + + 20 + + + 1 + + + + + PR + + + 2 + + + GREAT HEALTH PLAN + + + + + + + PI + + + PAYER12345 + + + + + 2 + + + 1 + + + 21 + + + 1 + + + + + 1P + + + 2 + + + COMMUNITY HOSPITAL + + + + + + + XX + + + 1234567890 + + + + + 3 + + + 2 + + + 22 + + + 0 + + + + + 1 + + + 987654321 + + + 1234567890 + + + + + IL + + + 1 + + + SMITH + + + JOHN + + + M + + + + + MI + + + SUB123456789 + + + + + D8 + + + 19800101 + + + M + + + + + 30 + + + + + 13 + + + 0001 + + + + + 1 + + + 1 + + + + + 1 + + + 000000001 + + + diff --git a/server/tests/test-ncpdp-51-request-input.txt b/server/tests/test-ncpdp-51-request-input.txt new file mode 100644 index 0000000000..7657c0435c --- /dev/null +++ b/server/tests/test-ncpdp-51-request-input.txt @@ -0,0 +1 @@ +00000051B1ASDF 1011234567890 20260913 AM01CY123456789C701AM04C2123456789AM07EM1D298765432E103D711111022222E728000D30D57D61D80DF0DJ3DE20260913AM03EZ01DB1234567890AM11D9136EDC80{DX00{DQ216EDU216EDN01 \ No newline at end of file diff --git a/server/tests/test-ncpdp-51-request-output.xml b/server/tests/test-ncpdp-51-request-output.xml new file mode 100644 index 0000000000..4e9b41e4eb --- /dev/null +++ b/server/tests/test-ncpdp-51-request-output.xml @@ -0,0 +1,50 @@ + + + 000000 + 51 + B1 + ASDF + 1 + 01 + 1234567890 + 20260913 + + + + 123456789 + 01 + + + 123456789 + + + + + 1 + 98765432 + 03 + 11111022222 + 28000 + 0 + 7 + 1 + 0 + 0 + 3 + 20260913 + + + 01 + 1234567890 + + + 136E + 80{ + 00{ + 216E + 216E + 01 + + + + diff --git a/server/tests/test-ncpdp-51-response-input.txt b/server/tests/test-ncpdp-51-response-input.txt new file mode 100644 index 0000000000..57f3661576 --- /dev/null +++ b/server/tests/test-ncpdp-51-response-input.txt @@ -0,0 +1 @@ +51B11A011234567890 20260913AM25C2123456789AM29AM21ANPF354716cb1bb9646a9bc8cAM22EM1D298765432AM23F500{F600{F7216EF9216EFM6 \ No newline at end of file diff --git a/server/tests/test-ncpdp-51-response-output.xml b/server/tests/test-ncpdp-51-response-output.xml new file mode 100644 index 0000000000..b312ec0964 --- /dev/null +++ b/server/tests/test-ncpdp-51-response-output.xml @@ -0,0 +1,34 @@ + + + 51 + B1 + 1 + A + 01 + 1234567890 + 20260913 + + + 123456789 + + + + + + P + 54716cb1bb9646a9bc8c + + + 1 + 98765432 + + + 00{ + 00{ + 216E + 216E + 6 + + + + diff --git a/server/tests/test-ncpdp-d0-request-input.txt b/server/tests/test-ncpdp-d0-request-input.txt new file mode 100644 index 0000000000..776ecb8a98 --- /dev/null +++ b/server/tests/test-ncpdp-d0-request-input.txt @@ -0,0 +1 @@ +000000D0B1ASDF 1011234567890 20260913 AM01CY123456789C701AM04C2123456789AM07EM1D298765432E103D711111022222E728000D30D57D61D80DF0DJ3DE20260913U75AM03EZ01DB1234567890AM11D9136EDC80{DX00{DQ216EDU216EDN01AM158CEX3QExample3U123 Anywhere St.5JNowhere3VIN6D12345 \ No newline at end of file diff --git a/server/tests/test-ncpdp-d0-request-output.xml b/server/tests/test-ncpdp-d0-request-output.xml new file mode 100644 index 0000000000..0346830984 --- /dev/null +++ b/server/tests/test-ncpdp-d0-request-output.xml @@ -0,0 +1,59 @@ + + + 000000 + D0 + B1 + ASDF + 1 + 01 + 1234567890 + 20260913 + + + + 123456789 + 01 + + + 123456789 + + + + + 1 + 98765432 + 03 + 11111022222 + 28000 + 0 + 7 + 1 + 0 + 0 + 3 + 20260913 + 5 + + + 01 + 1234567890 + + + 136E + 80{ + 00{ + 216E + 216E + 01 + + + EX + Example + 123 Anywhere St. + Nowhere + IN + 12345 + + + + diff --git a/server/tests/test-ncpdp-d0-response-input.txt b/server/tests/test-ncpdp-d0-response-input.txt new file mode 100644 index 0000000000..d2960b217b --- /dev/null +++ b/server/tests/test-ncpdp-d0-response-input.txt @@ -0,0 +1 @@ +D0B11A011234567890 20260913AM25C2123456789AM29AM21ANPF354716cb1bb9646a9bc8cAM22EM1D298765432AM23F500{F600{F7216EF9216EFM6 \ No newline at end of file diff --git a/server/tests/test-ncpdp-d0-response-output.xml b/server/tests/test-ncpdp-d0-response-output.xml new file mode 100644 index 0000000000..addf81dcee --- /dev/null +++ b/server/tests/test-ncpdp-d0-response-output.xml @@ -0,0 +1,34 @@ + + + D0 + B1 + 1 + A + 01 + 1234567890 + 20260913 + + + 123456789 + + + + + + P + 54716cb1bb9646a9bc8c + + + 1 + 98765432 + + + 00{ + 00{ + 216E + 216E + 6 + + + + diff --git a/smoketest/build.gradle b/smoketest/build.gradle index 4d121012c8..18e8b941bc 100644 --- a/smoketest/build.gradle +++ b/smoketest/build.gradle @@ -30,13 +30,26 @@ sourceSets { } } +// match ci/run-harness.sh, sort is critical +def engineHome = project(':server').file('setup') +def engineJars = files({ + fileTree(engineHome) { + include 'extensions/**/*.jar' + include 'server-lib/**/*.jar' + }.files.sort { it.path } +}) + // The root convention applies useJUnit() to every Test task; this module is JUnit 5. tasks.named('test') { useJUnitPlatform() // Only run our tests if a live server URL is provided // `./gradlew :smoketest:test -Doie.baseUrl=https://localhost:8443` onlyIf { System.getProperty('oie.baseUrl') != null } + // The thing under test is a running server, no incremental build is possible. + outputs.upToDateWhen { false } systemProperties System.getProperties().findAll { it.key.toString().startsWith('oie.') } + // Engine jars first, harness classes and fixtures last, as in the container. + classpath = engineJars + classpath } tasks.named('jar', Jar) { diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/BlockingResponseTransformerTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/BlockingResponseTransformerTest.java new file mode 100644 index 0000000000..0e9e87e88b --- /dev/null +++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/BlockingResponseTransformerTest.java @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Mitch Gaffigan + +package org.openintegrationengine.smoketest; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; + +import java.util.LinkedHashMap; +import java.util.UUID; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import com.mirth.connect.donkey.model.message.ConnectorMessage; +import com.mirth.connect.donkey.model.message.Status; + +/** + * A destination whose response transformer has not returned yet, the one part of + * ci/tests/190-response-handling that is not a fixture: the harness retries every fixture + * assertion until the message is terminal, so it can never see the PENDING the engine parks + * the destination at while the transformer runs. + * + *

Nothing here waits on a clock. The channel queues at its source, so the submit returns + * the message id straight away, and its response transformer parks until the gate property + * holds that message's token. The token is fresh per run, so a gate left set by an earlier run + * cannot release this one. + */ +@DisplayName("190-response-handling/06-blocking-response-transformer") +@TestInstance(PER_CLASS) +class BlockingResponseTransformerTest { + + private static final String CHANNEL = "channels/blocking-response-transformer.xml"; + + /** The configuration property the response transformer parks on. */ + private static final String GATE = "oie.smoketest.releaseResponse"; + + private static final int DESTINATION = 1; + + private String channelId; + + @BeforeAll + void deploy() throws Exception { + channelId = Harness.deploy(CHANNEL); + } + + @AfterAll + void undeploy() { + Harness.undeploy(channelId); + } + + @Test + void destinationStaysPendingUntilTheResponseTransformerReturns() throws Exception { + // The payload is this run's token: the destination echoes it back, so it is also the + // response the transformer parks on and the content it rewrites. + String token = UUID.randomUUID().toString(); + long messageId = SharedServer.get().submitMessage(channelId, token, new LinkedHashMap<>()); + + try { + ConnectorMessage blocked = Harness.awaitConnectorStatus(channelId, messageId, DESTINATION, Status.PENDING); + assertNotNull(blocked.getSent(), + "the destination sent its message before the response transformer ran, so the" + + " sent content should already be stored alongside the PENDING status"); + assertNull(blocked.getProcessedResponse(), + "the response transformer has not returned, so there is no processed response yet"); + } finally { + // Also on failure, so a wedged transformer never outlives the test. + Harness.setConfigurationProperty(GATE, token); + } + + ConnectorMessage finished = Harness.awaitConnectorStatus(channelId, messageId, DESTINATION, Status.SENT); + assertNotNull(finished.getProcessedResponse(), "no processed response was stored"); + assertEquals("released<" + token + ">", + MessageAssertions.responsePayload(finished.getProcessedResponse().getContent()), + "the response transformer's output should be the destination's processed response"); + } +} diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/CustomMetaDataColumnRedeployTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/CustomMetaDataColumnRedeployTest.java new file mode 100644 index 0000000000..ea4b5acb5a --- /dev/null +++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/CustomMetaDataColumnRedeployTest.java @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Mitch Gaffigan + +package org.openintegrationengine.smoketest; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; + +import java.math.BigDecimal; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import com.mirth.connect.donkey.model.channel.MetaDataColumn; +import com.mirth.connect.donkey.model.channel.MetaDataColumnType; +import com.mirth.connect.donkey.model.message.Status; +import com.mirth.connect.model.Channel; +import com.mirth.connect.model.converters.ObjectXMLSerializer; + +/** + * The half of custom metadata columns that ci/tests/200-custom-metadata-columns cannot express: a + * fixture deploys each channel once, so it only ever reaches {@code addMetaDataColumn}. Editing a + * deployed channel's column list and redeploying it is what reaches {@code removeMetaDataColumn}, + * and what turns a column's type change into a drop followed by an add. + */ +@DisplayName("200-custom-metadata-columns/02-redeploy") +@TestInstance(PER_CLASS) +class CustomMetaDataColumnRedeployTest { + + private static final String CHANNEL = "channels/custom-metadata-columns-redeploy.xml"; + + private String channelId; + + @AfterAll + void undeploy() { + Harness.undeploy(channelId); + } + + @Test + void redeployAddsRemovesAndRetypesColumns() throws Exception { + OieServer server = SharedServer.get(); + Channel channel = ObjectXMLSerializer.getInstance().deserialize(Harness.resource(CHANNEL), Channel.class); + + // The channel starts with KEPT, DROPPED and RETYPED, all STRING. + channelId = server.deployChannel(channel, CHANNEL); + long first = submit(server, "kept", "one", "dropped", "one", "retyped", "1.5"); + + Map firstMetaData = metaData(first); + assertEquals("one", firstMetaData.get("KEPT")); + assertEquals("one", firstMetaData.get("DROPPED")); + assertEquals("1.5", firstMetaData.get("RETYPED"), "a STRING column stores the value verbatim"); + + // Drop one column, retype another, add a third, and redeploy the same channel. + List columns = channel.getProperties().getMetaDataColumns(); + assertTrue(columns.removeIf(column -> "DROPPED".equals(column.getName())), + "Channel fixture has no DROPPED column"); + column(columns, "RETYPED").setType(MetaDataColumnType.NUMBER); + columns.add(new MetaDataColumn("ADDED", MetaDataColumnType.BOOLEAN, "added")); + server.deployChannel(channel, CHANNEL); + + long second = submit(server, "kept", "two", "dropped", "two", "retyped", "1.5", "added", "true"); + Map secondMetaData = metaData(second); + assertEquals("two", secondMetaData.get("KEPT")); + assertEquals(true, secondMetaData.get("ADDED"), "the column added on redeploy stores a BOOLEAN"); + assertFalse(secondMetaData.containsKey("DROPPED"), + () -> "DROPPED was removed from the channel but is still a column: " + secondMetaData.keySet()); + assertEquals(0, new BigDecimal("1.5").compareTo((BigDecimal) secondMetaData.get("RETYPED")), + "the retyped column now stores a NUMBER"); + + // The table survived both ALTER TABLEs, so the message sent before the redeploy is still + // readable - minus the column that was dropped, and minus the value in the column that was + // retyped, which a type change drops and re-adds. + Map firstAfterRedeploy = metaData(first); + assertEquals("one", firstAfterRedeploy.get("KEPT"), "an untouched column kept its stored value"); + assertFalse(firstAfterRedeploy.containsKey("DROPPED")); + assertNull(firstAfterRedeploy.get("RETYPED"), "a retyped column is dropped and re-added, losing old values"); + } + + /** Submits a message whose source map is the given key/value pairs. */ + private long submit(OieServer server, String... sourceMapEntries) throws Exception { + Map sourceMap = new LinkedHashMap<>(); + for (int i = 0; i < sourceMapEntries.length; i += 2) { + sourceMap.put(sourceMapEntries[i], sourceMapEntries[i + 1]); + } + return server.submitMessage(channelId, "Hello world!", sourceMap); + } + + /** Reads one message's source custom metadata back off the server. */ + private Map metaData(long messageId) throws Exception { + return Harness.awaitConnectorStatus(channelId, messageId, 0, Status.TRANSFORMED).getMetaDataMap(); + } + + private static MetaDataColumn column(List columns, String name) { + return columns.stream().filter(column -> name.equals(column.getName())).findFirst() + .orElseThrow(() -> new AssertionError("Channel fixture has no " + name + " column")); + } +} diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/DestinationQueueRotationTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/DestinationQueueRotationTest.java new file mode 100644 index 0000000000..635dc0aeb8 --- /dev/null +++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/DestinationQueueRotationTest.java @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Mitch Gaffigan + +package org.openintegrationengine.smoketest; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import com.mirth.connect.donkey.model.message.ConnectorMessage; +import com.mirth.connect.donkey.model.message.Status; + +/** + * A rotating destination queue steps over the message at its head rather than retrying it + * forever, so one message that never sends does not hold up the ones behind it. + * + *

The first message submitted is the one the destination never accepts, and it therefore + * owns the head of the queue for the whole test. Without rotation the queue thread would keep + * re-acquiring it and nothing else would ever be sent, so every later message reaching SENT is + * the rotation, not just a slow queue. + * + *

The order they are sent in is deliberately not asserted. Rotation moves the queue's starting + * point past whatever it just returned, so a message can come round on a later cycle than the one + * that was queued after it - a run of this has produced 2, 3, 4, 1. Reordering is what rotation + * is; the claim is that each message is sent exactly once. + * + *

That the stuck message is still queued is read once, after the others are all sent: by then + * the queue thread has demonstrably been past it, so nothing has to be inferred from a wait. + */ +@DisplayName("220-queueing/03-destination-queue-rotation") +@TestInstance(PER_CLASS) +class DestinationQueueRotationTest { + + private static final String CHANNEL = "channels/destination-queue-rotate.xml"; + + /** The configuration property the destination leaves messages queued on until it holds this run's token. */ + private static final String GATE = "oie.smoketest.releaseDestinationQueueRotate"; + + private static final int DESTINATION = 1; + + /** What the destination prefixes its handover position with. */ + private static final String POSITION = "order="; + + /** Messages behind the stuck one, which all send once the gate is open. */ + private static final int SENDABLE_COUNT = 4; + + private String channelId; + + @BeforeAll + void deploy() throws Exception { + channelId = Harness.deploy(CHANNEL); + } + + @AfterAll + void undeploy() { + Harness.undeploy(channelId); + } + + @Test + void aMessageThatNeverSendsDoesNotBlockTheOnesQueuedBehindIt() throws Exception { + // The payload carries this run's token, so a gate left open by an earlier run cannot + // release this one. The first message is the stuck one, and being first it is also the + // one the queue would otherwise acquire over and over. + String token = UUID.randomUUID().toString(); + long stuckMessageId; + List sendableMessageIds = new ArrayList<>(); + + try { + stuckMessageId = SharedServer.get().submitMessage(channelId, token + "#stuck", new LinkedHashMap<>()); + for (int i = 1; i <= SENDABLE_COUNT; i++) { + sendableMessageIds.add( + SharedServer.get().submitMessage(channelId, token + "#" + i, new LinkedHashMap<>())); + } + + Harness.awaitQueueSizeAtLeast(channelId, DESTINATION, SENDABLE_COUNT + 1); + } finally { + // Also on failure, so the gate is never left holding an earlier run's token. + Harness.setConfigurationProperty(GATE, token); + } + + List positions = new ArrayList<>(); + for (long messageId : sendableMessageIds) { + ConnectorMessage sent = Harness.awaitConnectorStatus(channelId, messageId, DESTINATION, Status.SENT); + String stamp = MessageAssertions.responsePayload(sent.getResponse().getContent()); + assertTrue(stamp != null && stamp.startsWith(POSITION), + "message " + messageId + " has an unexpected destination response: " + stamp); + positions.add(Integer.valueOf(stamp.substring(POSITION.length()))); + } + + // Sorted, because which cycle a message comes round on is rotation's business. That the + // positions are distinct and leave no gap is the claim: four messages, four handovers. + Collections.sort(positions); + List distinctPositions = new ArrayList<>(); + for (int i = 1; i <= SENDABLE_COUNT; i++) { + distinctPositions.add(i); + } + assertEquals(distinctPositions, positions, + "rotation should step over the stuck message and send each of the others exactly once"); + + // Every message behind it has been sent, so the queue thread has acquired and returned the + // stuck one at least that many times. It is still queued: rotation moved past it rather + // than failing it or dropping it. + assertEquals(Status.QUEUED, Harness.connectorMessage(channelId, stuckMessageId, DESTINATION).getStatus(), + "rotation should leave the stuck message on the queue, not end it"); + } +} diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/DestinationQueueTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/DestinationQueueTest.java new file mode 100644 index 0000000000..130e7dba2e --- /dev/null +++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/DestinationQueueTest.java @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Mitch Gaffigan + +package org.openintegrationengine.smoketest; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import com.mirth.connect.donkey.model.message.ConnectorMessage; +import com.mirth.connect.donkey.model.message.Status; + +/** + * A destination queue outlives the channel that owns it: messages a failing destination could + * not send are still queued after the channel is stopped and started, and the queue thread + * drains them in message order once the destination accepts them. + * + *

The destination's in-memory buffer is set to 3 and the test queues eight messages, so the + * queue the restarted channel drains cannot be the buffer it filled before the stop - it has to + * be reloaded from the database and paged through. + * + *

The gate stays shut across the whole stop and start, so every step here is a positive + * signal the server reports - queue depth, deployed state, connector status - and nothing is + * inferred from time passing. That a stopped channel does not send at all is a channel + * lifecycle claim rather than a queueing one, and is not asserted here. + */ +@DisplayName("220-queueing/02-destination-queue") +@TestInstance(PER_CLASS) +class DestinationQueueTest { + + private static final String CHANNEL = "channels/destination-queue.xml"; + + /** The configuration property the destination fails on until it holds this run's token. */ + private static final String GATE = "oie.smoketest.releaseDestinationQueue"; + + private static final int DESTINATION = 1; + + private static final int MESSAGE_COUNT = 8; + + private String channelId; + + @BeforeAll + void deploy() throws Exception { + channelId = Harness.deploy(CHANNEL); + } + + @AfterAll + void undeploy() { + Harness.undeploy(channelId); + } + + @Test + void queuedMessagesSurviveAChannelRestartAndDrainInOrder() throws Exception { + // The payload carries this run's token, so a gate left open by an earlier run cannot + // release this one, and an index, so a failure names the message that came out of order. + String token = UUID.randomUUID().toString(); + List messageIds = new ArrayList<>(); + + try { + for (int i = 1; i <= MESSAGE_COUNT; i++) { + messageIds.add(SharedServer.get().submitMessage(channelId, token + "#" + i, new LinkedHashMap<>())); + } + // The destination leaves every message on the queue while the gate is shut, so the + // queue is at its full depth - past the buffer it holds in memory - before the stop. + Harness.awaitQueueSizeAtLeast(channelId, DESTINATION, MESSAGE_COUNT); + + // Take the channel down and back up with the queue full and the gate still shut, so + // the queue the restarted channel drains is the one it reloaded from the database. + Harness.stopChannel(channelId); + Harness.startChannel(channelId); + + Harness.awaitQueueSizeAtLeast(channelId, DESTINATION, MESSAGE_COUNT); + } finally { + // Also on failure, so a channel left behind by this test is not a wedged one. + Harness.setConfigurationProperty(GATE, token); + } + + List observedOrder = new ArrayList<>(); + for (long messageId : messageIds) { + ConnectorMessage sent = Harness.awaitConnectorStatus(channelId, messageId, DESTINATION, Status.SENT); + observedOrder.add(MessageAssertions.responsePayload(sent.getResponse().getContent())); + } + + // The destination stamps each message with the position it was drained in, so the messages + // queued first must carry the lowest positions. + List queueOrder = new ArrayList<>(); + for (int i = 1; i <= MESSAGE_COUNT; i++) { + queueOrder.add("order=" + i); + } + assertEquals(queueOrder, observedOrder, "the destination queue should drain in message order"); + } +} diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/EncryptionAtRestTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/EncryptionAtRestTest.java new file mode 100644 index 0000000000..6dc4b5e655 --- /dev/null +++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/EncryptionAtRestTest.java @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Mitch Gaffigan + +package org.openintegrationengine.smoketest; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; + +import java.util.LinkedHashMap; +import java.util.List; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import com.mirth.connect.donkey.model.message.ConnectorMessage; +import com.mirth.connect.donkey.model.message.Message; +import com.mirth.connect.donkey.model.message.MessageContent; +import com.mirth.connect.donkey.model.message.Status; + +/** + * A channel with {@code encryptData} set stores its message content encrypted. No fixture can + * assert this: encryption is per-message randomised, so the stored value is different every run, + * and the only fixed part of it is the header the ciphertext carries. + * + *

Which of the two read APIs is used decides what comes back. {@code getMessages} with content + * leaves the DAO's decryption off, so it hands back the column as stored; {@code getMessageContent} + * decrypts. The two together are what makes "encrypted at rest" observable from a client. + */ +@DisplayName("210-encryption-at-rest") +@TestInstance(PER_CLASS) +class EncryptionAtRestTest { + + private static final String CHANNEL = "channels/encryption-at-rest.xml"; + + /** Prefix {@code KeyEncryptor} writes in front of every ciphertext, carrying the IV. */ + private static final String ENCRYPTION_HEADER = "{alg="; + + private static final String RAW = "Hello world!"; + /** The source transformer prefixes the message, so transformed and encoded differ from raw. */ + private static final String TRANSFORMED = "transformed:" + RAW; + + private static final int SOURCE = 0; + private static final int DESTINATION = 1; + + private String channelId; + + @BeforeAll + void deploy() throws Exception { + channelId = Harness.deploy(CHANNEL); + } + + @AfterAll + void undeploy() { + Harness.undeploy(channelId); + } + + @Test + void storesEveryContentStageEncrypted() throws Exception { + Message stored = process(); + ConnectorMessage source = connector(stored, SOURCE); + ConnectorMessage destination = connector(stored, DESTINATION); + + assertStoredEncrypted("source raw", source.getRaw()); + assertStoredEncrypted("source transformed", source.getTransformed()); + assertStoredEncrypted("source encoded", source.getEncoded()); + // The destination's own content, written by a different DAO call than the source's. + assertStoredEncrypted("destination sent", destination.getSent()); + } + + @Test + void decryptsContentWhenReadBack() throws Exception { + Message stored = process(); + ConnectorMessage source = connector( + SharedServer.get().fetchDecryptedMessage(channelId, stored.getMessageId(), + List.of(SOURCE, DESTINATION)), + SOURCE); + + assertDecrypted("source raw", source.getRaw(), RAW); + assertDecrypted("source transformed", source.getTransformed(), TRANSFORMED); + assertDecrypted("source encoded", source.getEncoded(), TRANSFORMED); + } + + /** Submits one message and returns it as stored, once the destination has finished with it. */ + private Message process() throws Exception { + OieServer server = SharedServer.get(); + long messageId = server.submitMessage(channelId, RAW, new LinkedHashMap<>()); + Harness.awaitConnectorStatus(channelId, messageId, DESTINATION, Status.SENT); + return server.fetchMessage(channelId, messageId); + } + + /** + * Asserts that what the server stored is ciphertext: flagged encrypted, carrying the + * encryptor's header, and not the plaintext it was made from. + */ + private static void assertStoredEncrypted(String label, MessageContent content) { + assertNotNull(content, () -> label + " was not stored at all"); + String stored = content.getContent(); + assertTrue(content.isEncrypted(), () -> label + " is not flagged as encrypted: " + stored); + assertTrue(stored.startsWith(ENCRYPTION_HEADER), + () -> label + " does not carry an encryption header: " + stored); + assertFalse(stored.contains(RAW), () -> label + " leaks the plaintext payload: " + stored); + } + + /** Asserts that the decrypting read path recovers the original content. */ + private static void assertDecrypted(String label, MessageContent content, String expected) { + assertNotNull(content, () -> label + " was not returned by the decrypting read"); + assertEquals(expected, content.getContent(), () -> label + " did not decrypt to its original"); + assertFalse(content.isEncrypted(), () -> label + " is still flagged as encrypted after decryption"); + } + + private static ConnectorMessage connector(Message message, int metaDataId) { + assertNotNull(message, "The server returned no message"); + ConnectorMessage connectorMessage = message.getConnectorMessages().get(metaDataId); + assertNotNull(connectorMessage, + () -> "Message " + message.getMessageId() + " has no connector " + metaDataId + + "; present ids: " + message.getConnectorMessages().keySet()); + return connectorMessage; + } +} diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/Harness.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/Harness.java index 5a2b702819..513b50fdef 100644 --- a/smoketest/src/test/java/org/openintegrationengine/smoketest/Harness.java +++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/Harness.java @@ -6,7 +6,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; @@ -18,6 +20,7 @@ import com.mirth.connect.donkey.model.message.Message; import com.mirth.connect.donkey.model.message.MessageContent; import com.mirth.connect.donkey.model.message.Status; +import com.mirth.connect.donkey.model.message.attachment.Attachment; /** * The entry points the generated smoke tests call (see :smoketest:generateSmokeTests @@ -28,6 +31,16 @@ public final class Harness { /** Statuses that mean the server has not finished with the message yet. */ private static final List PENDING_STATUSES = List.of(Status.PENDING, Status.QUEUED); + /** + * How long to keep retrying after the message looks terminal. Some work outruns the + * statuses: a channel with a queued destination and removeContentOnCompletion deletes + * its content in a transaction committed after the destination is already SENT. + */ + private static final Duration TERMINAL_GRACE = Duration.ofSeconds(5); + + /** How often to re-read a message while waiting for it to reach a state. */ + private static final Duration POLL_INTERVAL = Duration.ofMillis(250); + private Harness() { } @@ -63,9 +76,9 @@ public static void undeploy(String channelId) { /** * Submits {@code /source} (with {@code /source_sourcemap.yml} when * {@code hasSourceMap}) into the channel, then retries the named assertion files until - * they all hold or the message reaches a terminal state. Because the message is written - * asynchronously, an early poll can legitimately fail; only a failure that persists once - * the message is terminal is a real failure. + * they all hold or the message has been terminal for {@link #TERMINAL_GRACE}. Because the + * message is written asynchronously, an early poll can legitimately fail; only a failure + * that outlives the message's terminal state is a real failure. */ public static void runMessage(String channelId, String base, boolean hasSourceMap, String... assertionFiles) throws Exception { @@ -76,8 +89,11 @@ public static void runMessage(String channelId, String base, boolean hasSourceMa // Load the fixtures once; the poll loop below may check them many times. Map assertions = new LinkedHashMap<>(); + // Attachments are a second read against the server, so only pay for it when asked. + boolean fetchAttachments = false; for (String fileName : assertionFiles) { - assertions.put(fileName, resource(base + "/" + fileName)); + assertions.put(fileName, resource(base + "/" + fileName, MessageAssertions.charsetFor(fileName))); + fetchAttachments |= MessageAssertions.isAttachmentFixture(fileName); } long messageId = server().submitMessage(channelId, source, sourceMap); @@ -85,40 +101,192 @@ public static void runMessage(String channelId, String base, boolean hasSourceMa long deadline = System.nanoTime() + HarnessConfig.TIMEOUT.toNanos(); AssertionError lastFailure = null; Message lastMessage = null; + List lastAttachments = List.of(); + long graceDeadline = 0; while (System.nanoTime() < deadline) { Message message = server().fetchMessage(channelId, messageId); if (message != null) { lastMessage = message; + List attachments = fetchAttachments + ? server().fetchAttachments(channelId, messageId) + : List.of(); + lastAttachments = attachments; try { for (Map.Entry assertion : assertions.entrySet()) { - MessageAssertions.assertFixtureFile(message, assertion.getKey(), assertion.getValue()); + MessageAssertions.assertFixtureFile(message, attachments, assertion.getKey(), + assertion.getValue()); } return; } catch (AssertionError e) { lastFailure = e; if (isTerminal(message)) { - break; + if (graceDeadline == 0) { + graceDeadline = System.nanoTime() + TERMINAL_GRACE.toNanos(); + } else if (System.nanoTime() >= graceDeadline) { + break; + } } } } - Thread.sleep(500); + Thread.sleep(POLL_INTERVAL.toMillis()); } if (lastFailure != null) { throw new AssertionError(base + " failed: " + lastFailure.getMessage() - + "\n\n" + describe(lastMessage), lastFailure); + + "\n\n" + describe(lastMessage, lastAttachments), lastFailure); } throw new AssertionError("Timed out after " + HarnessConfig.TIMEOUT.toSeconds() + "s waiting for message " - + messageId + " for fixture " + base + "\n\n" + describe(lastMessage)); + + messageId + " for fixture " + base + "\n\n" + describe(lastMessage, lastAttachments)); + } + + /** + * Polls until one connector of one message reaches {@code status}, and returns it. Java + * tests need this where {@link #runMessage} cannot help: it stops at the first terminal + * state, so it can never observe a message mid-flight. + */ + public static ConnectorMessage awaitConnectorStatus(String channelId, long messageId, int metaDataId, + Status status) throws Exception { + long deadline = System.nanoTime() + HarnessConfig.TIMEOUT.toNanos(); + Status lastStatus = null; + do { + Message message = server().fetchMessage(channelId, messageId); + ConnectorMessage connectorMessage = message == null ? null + : message.getConnectorMessages().get(metaDataId); + if (connectorMessage != null) { + lastStatus = connectorMessage.getStatus(); + if (lastStatus == status) { + return connectorMessage; + } + } + Thread.sleep(POLL_INTERVAL.toMillis()); + } while (System.nanoTime() < deadline); + + throw new AssertionError("Timed out after " + HarnessConfig.TIMEOUT.toSeconds() + "s waiting for connector " + + metaDataId + " of message " + messageId + " to reach " + status + "; last status was " + lastStatus); + } + + /** + * Polls until the server has marked one message processed, and returns it. A connector reaching + * its final status is not the end of the message: the engine still has the postprocessor to run + * and the message row to mark, and that last commit is where the remaining statistics land. A + * test that reads anything channel-wide has to wait for it. + */ + public static Message awaitProcessed(String channelId, long messageId) throws Exception { + long deadline = System.nanoTime() + HarnessConfig.TIMEOUT.toNanos(); + Message message = null; + do { + message = server().fetchMessage(channelId, messageId); + if (message != null && message.isProcessed()) { + return message; + } + Thread.sleep(POLL_INTERVAL.toMillis()); + } while (System.nanoTime() < deadline); + + throw new AssertionError("Timed out after " + HarnessConfig.TIMEOUT.toSeconds() + "s waiting for message " + + messageId + " of channel " + channelId + " to be processed\n\n" + describe(message, List.of())); + } + + /** + * Polls until at least {@code minimum} messages are queued for one connector. Queue tests use + * this to know a queue has really built up - past its in-memory buffer, say - before releasing + * whatever is holding it, so that the depth is a precondition the test enforces rather than one + * it hopes for. + */ + public static void awaitQueueSizeAtLeast(String channelId, int metaDataId, long minimum) throws Exception { + long deadline = System.nanoTime() + HarnessConfig.TIMEOUT.toNanos(); + Long lastSize = null; + do { + lastSize = server().queueSize(channelId, metaDataId); + if (lastSize != null && lastSize >= minimum) { + return; + } + Thread.sleep(POLL_INTERVAL.toMillis()); + } while (System.nanoTime() < deadline); + + throw new AssertionError("Timed out after " + HarnessConfig.TIMEOUT.toSeconds() + "s waiting for connector " + + metaDataId + " of channel " + channelId + " to have at least " + minimum + + " messages queued; last size was " + lastSize); } - /** Reads a staged fixture from the classpath. */ + /** + * Polls until a connector's queue has fallen to {@code maximum} messages or fewer. The mirror of + * {@link #awaitQueueSizeAtLeast}, for a test that emptied a queue rather than filled one. + */ + public static void awaitQueueSizeAtMost(String channelId, int metaDataId, long maximum) throws Exception { + long deadline = System.nanoTime() + HarnessConfig.TIMEOUT.toNanos(); + Long lastSize = null; + do { + lastSize = server().queueSize(channelId, metaDataId); + if (lastSize != null && lastSize <= maximum) { + return; + } + Thread.sleep(POLL_INTERVAL.toMillis()); + } while (System.nanoTime() < deadline); + + throw new AssertionError("Timed out after " + HarnessConfig.TIMEOUT.toSeconds() + "s waiting for connector " + + metaDataId + " of channel " + channelId + " to have at most " + maximum + + " messages queued; last size was " + lastSize); + } + + /** + * Reads one connector of one message as it stands right now, without waiting for anything. + * This is for asserting where a message has not got to, which is only sound once + * something else has proved the engine went past it - never on its own, as a message that has + * simply not been picked up yet looks identical. + */ + public static ConnectorMessage connectorMessage(String channelId, long messageId, int metaDataId) + throws Exception { + Message message = server().fetchMessage(channelId, messageId); + ConnectorMessage connectorMessage = message == null ? null + : message.getConnectorMessages().get(metaDataId); + if (connectorMessage == null) { + throw new AssertionError("Message " + messageId + " of channel " + channelId + + " has no connector " + metaDataId); + } + return connectorMessage; + } + + /** Stops a deployed channel, so its queue threads are no longer running. */ + public static void stopChannel(String channelId) throws Exception { + server().stopChannel(channelId); + } + + /** Halts a deployed channel, interrupting whatever it is processing rather than waiting. */ + public static void haltChannel(String channelId) throws Exception { + server().haltChannel(channelId); + } + + /** Starts a stopped channel back up. */ + public static void startChannel(String channelId) throws Exception { + server().startChannel(channelId); + } + + /** Undeploys a channel without removing it, leaving everything it stored in place. */ + public static void undeployChannel(String channelId) throws Exception { + server().undeployChannel(channelId); + } + + /** Sets one configuration map entry, which channel scripts read back as {@code configurationMap}. */ + public static void setConfigurationProperty(String key, String value) throws Exception { + server().setConfigurationProperty(key, value); + } + + /** Reads a staged fixture from the classpath as UTF-8 text. */ static String resource(String path) { + return resource(path, StandardCharsets.UTF_8); + } + + /** + * Reads a staged fixture from the classpath. Most fixtures are text and are read as UTF-8; + * an attachment's content can be any bytes at all, so it is read as ISO-8859-1, which maps + * every byte to one char and back again and so compares byte for byte. + */ + static String resource(String path, Charset charset) { try (InputStream in = Harness.class.getClassLoader().getResourceAsStream(path)) { if (in == null) { throw new IllegalStateException("Missing fixture resource on the classpath: " + path); } - return new String(in.readAllBytes(), StandardCharsets.UTF_8); + return new String(in.readAllBytes(), charset); } catch (IOException e) { throw new UncheckedIOException("Could not read fixture resource " + path, e); } @@ -138,7 +306,7 @@ private static boolean isTerminal(Message message) { } /** Renders the message the way a fixture author needs to see it to fix a mismatch. */ - private static String describe(Message message) { + private static String describe(Message message, List attachments) { if (message == null) { return "No message was retrieved from the server."; } @@ -159,12 +327,19 @@ private static String describe(Message message) { appendContent(detail, "encoded", connectorMessage.getEncoded()); appendContent(detail, "sent", connectorMessage.getSent()); appendContent(detail, "response", connectorMessage.getResponse()); + appendContent(detail, "responseTransformed", connectorMessage.getResponseTransformed()); + appendContent(detail, "processedResponse", connectorMessage.getProcessedResponse()); detail.append("\n connectorMap=").append(connectorMessage.getConnectorMap()) .append("\n metaDataMap=").append(connectorMessage.getMetaDataMap()); if (connectorMessage.getProcessingError() != null) { detail.append("\n processingError=").append(connectorMessage.getProcessingError()); } }); + MessageAssertions.order(message, attachments).forEach(attachment -> detail + .append("\n attachment ").append(attachment.getId()) + .append(" type=").append(attachment.getType()) + .append(" content=") + .append(quote(new String(attachment.getContent(), StandardCharsets.ISO_8859_1)))); return detail.toString(); } diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/HarnessConfig.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/HarnessConfig.java index 59fef4fe22..0de84fbae1 100644 --- a/smoketest/src/test/java/org/openintegrationengine/smoketest/HarnessConfig.java +++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/HarnessConfig.java @@ -37,11 +37,17 @@ final class HarnessConfig { Duration.ofSeconds(Long.parseLong(System.getProperty("oie.timeoutSeconds", "90"))); /** - * Per-request socket timeout. {@code new Client(address)} defaults to an infinite - * timeout, which would let a wedged server hang CI instead of failing it. + * Per-request socket timeout. {@code new Client(address)} defaults to an infinite timeout, + * which would let a wedged server hang CI instead of failing it. + * + *

This has to clear the slowest thing one request can legitimately queue behind, not the + * time a request normally takes: test classes run in parallel against one server, and a + * channel deploy holds it long enough that an unrelated {@code getChannelStatus} can wait + * seconds. {@link #TIMEOUT} still bounds the test as a whole, so a genuinely wedged server + * fails - just not on the first read that was merely waiting its turn. */ static final int REQUEST_TIMEOUT_MILLIS = - Integer.parseInt(System.getProperty("oie.requestTimeoutMillis", "15000")); + Integer.parseInt(System.getProperty("oie.requestTimeoutMillis", "30000")); private HarnessConfig() { } diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/MessageAssertions.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/MessageAssertions.java index f9849435ae..062ce8421f 100644 --- a/smoketest/src/test/java/org/openintegrationengine/smoketest/MessageAssertions.java +++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/MessageAssertions.java @@ -3,7 +3,15 @@ package org.openintegrationengine.smoketest; +import java.math.BigDecimal; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Comparator; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.regex.Matcher; @@ -16,6 +24,7 @@ import com.mirth.connect.donkey.model.message.MessageContent; import com.mirth.connect.donkey.model.message.Response; import com.mirth.connect.donkey.model.message.Status; +import com.mirth.connect.donkey.model.message.attachment.Attachment; import com.mirth.connect.model.converters.ObjectXMLSerializer; /** @@ -32,10 +41,27 @@ final class MessageAssertions { /** Fixture wildcard: matches any run of characters, for timestamps and generated ids. */ private static final String ANY_WILDCARD = "((ANY))"; + /** + * Fixture sentinel asserting that nothing is stored: a whole content file, or one key of a + * metadata file. + */ + private static final String NONE_SENTINEL = "((NONE))"; + private static final Pattern RESPONSE_ENVELOPE = Pattern.compile("^\\s*].*", Pattern.DOTALL); + /** + * Attachment assertion files are {@code attachment} plus an optional suffix, where + * {@code NN} is the attachment's position in the message rather than its id, which is a + * UUID the server generates. + */ + private static final Pattern ATTACHMENT_NAME = Pattern.compile("attachment(\\d+)(_type)?"); + + /** The token an attachment handler leaves in the message where it took an attachment out. */ + private static final Pattern ATTACHMENT_TOKEN = Pattern.compile("\\$\\{ATTACH:([^}]+)\\}"); + /** Destination assertion files are {@code dest} plus an optional suffix. */ - private static final Pattern DEST_NAME = Pattern.compile("dest(\\d+)(_transformed|_response|_status|_metadata\\.yml)?"); + private static final Pattern DEST_NAME = Pattern.compile( + "dest(\\d+)(_transformed|_response|_processed_response|_processing_error|_response_error|_status|_metadata\\.yml)?"); /** Source connector metadata id; destination N is metadata id N. */ private static final int SOURCE_META_DATA_ID = 0; @@ -43,26 +69,108 @@ final class MessageAssertions { private MessageAssertions() { } + /** + * True for a fixture file that asserts an attachment rather than something on the message. + * Attachments are stored in their own table, so the harness has to ask the server for them + * separately, and only does so when a fixture names one. + */ + static boolean isAttachmentFixture(String fileName) { + return ATTACHMENT_NAME.matcher(fileName).matches(); + } + + /** + * The charset a fixture file is read with. An attachment's content is arbitrary bytes - an + * image, a DICOM object - so it is read as ISO-8859-1, which maps every byte to one char and + * back without loss, making the comparison a byte-for-byte one. Everything else is text. + */ + static Charset charsetFor(String fileName) { + Matcher matcher = ATTACHMENT_NAME.matcher(fileName); + return matcher.matches() && matcher.group(2) == null ? StandardCharsets.ISO_8859_1 + : StandardCharsets.UTF_8; + } + /** * Applies one fixture file's assertion to the message. * - * @param fileName the fixture file name, e.g. {@code source_status} or {@code dest01} - * @param content that file's text, already loaded from the classpath + * @param attachments the message's attachments, empty unless a fixture asked for them + * @param fileName the fixture file name, e.g. {@code source_status} or {@code dest01} + * @param content that file's text, already loaded from the classpath */ - static void assertFixtureFile(Message message, String fileName, String content) { + static void assertFixtureFile(Message message, List attachments, String fileName, String content) { + if (isAttachmentFixture(fileName)) { + assertAttachment(message, attachments, fileName, content); + return; + } switch (fileName) { case "source_status" -> assertStatus("source status", content, connector(message, SOURCE_META_DATA_ID, fileName).getStatus()); + case "source_raw" -> assertContent("source raw", content, + content(connector(message, SOURCE_META_DATA_ID, fileName).getRaw())); case "source_transformed" -> assertContent("source transformed", content, content(connector(message, SOURCE_META_DATA_ID, fileName).getTransformed())); + case "source_encoded" -> assertContent("source encoded", content, + content(connector(message, SOURCE_META_DATA_ID, fileName).getEncoded())); + case "source_processing_error" -> assertContent("source processing error", content, + connector(message, SOURCE_META_DATA_ID, fileName).getProcessingError()); case "source_response" -> assertResponse("source response", content, - connector(message, SOURCE_META_DATA_ID, fileName)); + connector(message, SOURCE_META_DATA_ID, fileName).getResponse()); case "source_metadata.yml" -> assertMetadata("source_metadata.yml", parseYamlMap(content), connector(message, SOURCE_META_DATA_ID, fileName)); default -> assertDestination(message, fileName, content); } } + /** + * Asserts one attachment's content or mime type. {@code attachment01} is the attachment whose + * token appears first in the source raw content, not the first the server hands back: that + * list is ordered by id, which is a generated UUID and so bears no relation to the message. + * An attachment the message does not reference sorts after the ones it does, by id, so a + * handler that stores attachments without leaving tokens behind still has a stable order. + */ + private static void assertAttachment(Message message, List attachments, String fileName, + String content) { + Matcher matcher = ATTACHMENT_NAME.matcher(fileName); + if (!matcher.matches()) { + throw new IllegalStateException("Unrecognised fixture file name: " + fileName); + } + int position = Integer.parseInt(matcher.group(1)); + List ordered = order(message, attachments); + Attachment attachment = position >= 1 && position <= ordered.size() ? ordered.get(position - 1) : null; + + if ("_type".equals(matcher.group(2))) { + assertMatches(fileName, content.trim(), attachment == null ? null : attachment.getType()); + } else { + assertMatches(fileName, content, attachment == null ? null + : new String(attachment.getContent(), StandardCharsets.ISO_8859_1)); + } + } + + /** + * Puts a message's attachments into the order a fixture numbers them by: the order their + * tokens appear in the source raw content, then whatever is left over, by id. + */ + static List order(Message message, List attachments) { + Map byId = new LinkedHashMap<>(); + attachments.forEach(attachment -> byId.put(attachment.getId(), attachment)); + + List ordered = new ArrayList<>(); + Map connectorMessages = message.getConnectorMessages(); + ConnectorMessage source = connectorMessages == null ? null : connectorMessages.get(SOURCE_META_DATA_ID); + String raw = source == null ? null : content(source.getRaw()); + if (raw != null) { + Matcher tokens = ATTACHMENT_TOKEN.matcher(raw); + while (tokens.find()) { + Attachment referenced = byId.remove(tokens.group(1)); + if (referenced != null) { + ordered.add(referenced); + } + } + } + + byId.values().stream().sorted(Comparator.comparing(Attachment::getId)).forEach(ordered::add); + return ordered; + } + private static void assertDestination(Message message, String fileName, String content) { Matcher matcher = DEST_NAME.matcher(fileName); if (!matcher.matches()) { @@ -76,7 +184,10 @@ private static void assertDestination(Message message, String fileName, String c switch (suffix) { case "" -> assertContent(fileName, content, content(destination.getSent())); case "_transformed" -> assertContent(fileName, content, content(destination.getTransformed())); - case "_response" -> assertResponse(fileName, content, destination); + case "_response" -> assertResponse(fileName, content, destination.getResponse()); + case "_processed_response" -> assertResponse(fileName, content, destination.getProcessedResponse()); + case "_processing_error" -> assertContent(fileName, content, destination.getProcessingError()); + case "_response_error" -> assertContent(fileName, content, destination.getResponseError()); case "_status" -> assertStatus(fileName, content, destination.getStatus()); case "_metadata.yml" -> assertMetadata(fileName, parseYamlMap(content), destination); default -> throw new IllegalStateException("Unhandled fixture suffix: " + suffix); @@ -119,15 +230,18 @@ private static void assertContent(String label, String expected, String actual) * fixture actually describes. Line endings are normalised because HL7 acknowledgements * come back CR-delimited while the fixture files are LF-delimited. */ - private static void assertResponse(String label, String expected, ConnectorMessage connectorMessage) { - String stored = content(connectorMessage.getResponse()); - String actual = stored; - if (stored != null && RESPONSE_ENVELOPE.matcher(stored).matches()) { - Response response = ObjectXMLSerializer.getInstance().deserialize(stored.trim(), Response.class); - String payload = response == null ? null : response.getMessage(); - actual = payload == null ? null : payload.replace("\r\n", "\n").replace('\r', '\n'); + private static void assertResponse(String label, String expected, MessageContent responseContent) { + assertMatches(label, expected, responsePayload(content(responseContent))); + } + + /** Unwraps a stored {@link Response} to the payload a fixture describes, or passes it through. */ + static String responsePayload(String stored) { + if (stored == null || !RESPONSE_ENVELOPE.matcher(stored).matches()) { + return stored; } - assertMatches(label, expected, actual); + Response response = ObjectXMLSerializer.getInstance().deserialize(stored.trim(), Response.class); + String payload = response == null ? null : response.getMessage(); + return payload == null ? null : payload.replace("\r\n", "\n").replace('\r', '\n'); } /** @@ -153,13 +267,23 @@ private static void assertSubset(String label, String path, Map Map actual) { for (Map.Entry entry : expected.entrySet()) { String keyPath = path.isEmpty() ? entry.getKey() : path + "." + entry.getKey(); + Object expectedValue = entry.getValue(); + Object actualValue = actual.get(entry.getKey()); + + // A key whose value is the sentinel asserts the opposite: nothing was stored for it. + if (NONE_SENTINEL.equals(expectedValue)) { + if (actualValue != null) { + throw new AssertionError("Metadata mismatch for " + label + " at " + keyPath + + ": expected no value, found " + describe(actualValue)); + } + continue; + } + if (!actual.containsKey(entry.getKey())) { throw new AssertionError("Metadata mismatch for " + label + ": missing key " + keyPath + "; present keys: " + actual.keySet()); } - Object expectedValue = entry.getValue(); - Object actualValue = actual.get(entry.getKey()); if (expectedValue instanceof Map expectedMap) { if (!(actualValue instanceof Map actualMap)) { throw new AssertionError("Metadata mismatch for " + label + " at " + keyPath @@ -181,11 +305,41 @@ private static boolean scalarsEqual(Object expected, Object actual) { if (expected == null || actual == null) { return Objects.equals(expected, actual); } - return String.valueOf(expected).equals(String.valueOf(actual)); + return scalarText(expected).equals(scalarText(actual)); + } + + /** + * Renders a scalar the way a fixture writes it. A TIMESTAMP custom metadata column comes back + * as a {@link Calendar}, whose {@code toString} spells out every field and the JVM's time zone, + * so it is rendered as its UTC instant instead: a fixture writes {@code 2010-01-02T13:01:02Z}. + * + *

A NUMBER column comes back as a {@link BigDecimal} whose scale is the dialect's business: + * the column is {@code DECIMAL(31, 15)} everywhere, and derby, postgres, mysql and sqlserver + * all return the column's scale, so {@code 1234.5678} arrives as {@code 1234.567800000000000}, + * while oracle returns the scale the value was stored with. Trailing zeros are therefore + * dropped, which leaves a fixture free to write the value it sent. + */ + private static String scalarText(Object value) { + if (value instanceof Calendar calendar) { + return Instant.ofEpochMilli(calendar.getTimeInMillis()).toString(); + } + if (value instanceof BigDecimal number) { + return number.stripTrailingZeros().toPlainString(); + } + return String.valueOf(value); } - /** Compares an assertion file to actual content, honouring {@value #ANY_WILDCARD}. */ + /** + * Compares an assertion file to actual content, honouring {@value #ANY_WILDCARD} and + * {@value #NONE_SENTINEL}. + */ private static void assertMatches(String label, String expected, String actual) { + if (NONE_SENTINEL.equals(expected.trim())) { + if (actual != null) { + throw new AssertionError("Expected " + label + " content to be absent, found " + describe(actual)); + } + return; + } if (actual == null) { throw new AssertionError("Expected " + label + " content but the server stored none"); } @@ -222,7 +376,7 @@ private static String content(MessageContent messageContent) { } private static String describe(Object value) { - return value == null ? "" : "\"" + value + "\""; + return value == null ? "" : "\"" + scalarText(value) + "\""; } @SuppressWarnings("unchecked") diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/MessageDeletionTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/MessageDeletionTest.java new file mode 100644 index 0000000000..b55c57df71 --- /dev/null +++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/MessageDeletionTest.java @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Mitch Gaffigan + +package org.openintegrationengine.smoketest; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; + +import com.mirth.connect.donkey.model.message.ConnectorMessage; +import com.mirth.connect.donkey.model.message.Message; +import com.mirth.connect.donkey.model.message.Status; +import com.mirth.connect.donkey.model.message.attachment.Attachment; +import com.mirth.connect.model.ChannelStatistics; + +/** + * Message pruning and deletion over the client API: the three deletes the message browser offers, + * and what each one takes with it. There is no fixture shape for this - a fixture submits a message + * and asserts what was stored, where every assertion here is about what is no longer stored after a + * later call. + * + *

The channel carries an attachment per message, a custom metadata column, and two destinations, + * so every table a delete has to cascade into (content, attachments, custom metadata, connector + * messages) holds a row before the delete runs. The cascades themselves are the point: they are + * separate statements per dialect, and a database that keeps real foreign keys refuses the parent + * delete if a child row is left behind, so a delete that returns at all has already proved most of + * its own cascade. + */ +@DisplayName("240-message-deletion/01-cascades") +@TestInstance(PER_CLASS) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class MessageDeletionTest { + + private static final String CHANNEL = "channels/message-deletion.xml"; + + /** Two regex attachments and a custom metadata value, so a delete has something to cascade into. */ + private static final String PAYLOAD = "PHOTO:snapshot bytes;REPORT:written up;body"; + + /** What {@link #PAYLOAD} looks like once stored: the attachment handler has taken both values out. */ + private static final Pattern STORED_PAYLOAD = Pattern + .compile("PHOTO:\\$\\{ATTACH:[0-9a-f-]+\\};REPORT:\\$\\{ATTACH:[0-9a-f-]+\\};body"); + + /** The last destination in the chain; once it is SENT the message is done. */ + private static final int LAST_DESTINATION = 2; + + private String channelId; + + @BeforeAll + void deploy() throws Exception { + channelId = Harness.deploy(CHANNEL); + } + + @AfterAll + void undeploy() { + Harness.undeploy(channelId); + } + + @Test + @Order(1) + @DisplayName("deleting one message takes its content, metadata and attachments with it") + void deleteOneMessage() throws Exception { + OieServer server = SharedServer.get(); + long deleted = submit("deleted"); + long kept = submit("kept"); + + assertEquals(2, attachments(deleted).size(), "the channel should have stored both attachments"); + + server.removeMessage(channelId, deleted, null); + + assertNull(server.fetchMessage(channelId, deleted), "the message row survived the delete"); + assertEquals(List.of(), attachments(deleted), "the message's attachments outlived the message"); + + // The delete was scoped to one message id, not to the channel. + Message survivor = server.fetchMessage(channelId, kept); + assertNotNull(survivor, "deleting one message deleted another one too"); + assertEquals("kept", survivor.getConnectorMessages().get(0).getMetaDataMap().get("MDSTRING")); + assertEquals(2, attachments(kept).size(), "deleting one message took another message's attachments"); + } + + @Test + @Order(2) + @DisplayName("deleting one connector message leaves the rest of the message") + void deleteConnectorMessage() throws Exception { + OieServer server = SharedServer.get(); + long messageId = submit("partial"); + + server.removeMessage(channelId, messageId, LAST_DESTINATION); + + Message message = server.fetchMessage(channelId, messageId); + assertNotNull(message, "deleting a destination's connector message deleted the whole message"); + Map connectors = message.getConnectorMessages(); + assertFalse(connectors.containsKey(LAST_DESTINATION), + () -> "destination " + LAST_DESTINATION + " is still there: " + connectors.keySet()); + assertTrue(connectors.containsKey(0) && connectors.containsKey(1), + () -> "the source and the other destination should be untouched, but got " + connectors.keySet()); + + // The remaining connectors kept their own rows in every table the cascade touched. + ConnectorMessage source = connectors.get(0); + assertStoredPayload(source.getRaw().getContent(), "the source's content went with the destination's"); + assertEquals("partial", source.getMetaDataMap().get("MDSTRING"), + "the source's custom metadata went with the destination's"); + assertNotNull(connectors.get(1).getEncoded(), "destination 1's content went with destination 2's"); + assertEquals(2, attachments(messageId).size(), + "attachments belong to the message, not to one connector, so a connector delete keeps them"); + } + + @Test + @Order(3) + @DisplayName("removing all messages empties the channel and leaves it usable") + void removeAllMessages() throws Exception { + OieServer server = SharedServer.get(); + long messageId = submit("bulk"); + assertTrue(server.messageCount(channelId) > 0, "nothing to remove"); + ChannelStatistics before = server.statistics(channelId); + + server.removeAllMessages(channelId, false); + + assertEquals(0, server.messageCount(channelId), "the channel still holds messages"); + assertEquals(List.of(), attachments(messageId), "the attachment table was not truncated with the messages"); + + // Without clearStatistics the counters are a lifetime total and survive the delete. + assertStatistics(before, server.statistics(channelId), + "removing messages should not have touched the statistics"); + + // Emptying the channel drops and re-adds the foreign keys between its message tables; a + // message that processes and reads back afterwards is what proves they were all restored. + long after = submit("after-bulk-delete"); + Message message = server.fetchMessage(channelId, after); + assertNotNull(message, "the channel stored nothing after all its messages were removed"); + assertStoredPayload(message.getConnectorMessages().get(0).getRaw().getContent(), + "the message stored after the bulk delete did not come back intact"); + assertEquals(2, attachments(after).size()); + } + + @Test + @Order(4) + @DisplayName("removing all messages can reset the channel's statistics at the same time") + void removeAllMessagesClearingStatistics() throws Exception { + OieServer server = SharedServer.get(); + submit("statistics"); + ChannelStatistics before = server.statistics(channelId); + assertTrue(before.getReceived() > 0 && before.getSent() > 0, + () -> "the channel should have counted the messages it processed, but got " + before); + + server.removeAllMessages(channelId, true); + + assertEquals(0, server.messageCount(channelId)); + assertStatistics(new ChannelStatistics(), server.statistics(channelId), + "clearing the statistics should have zeroed every counter"); + } + + /** Submits one message whose {@code MDSTRING} column holds {@code label}, and waits for it to finish. */ + private long submit(String label) throws Exception { + Map sourceMap = new LinkedHashMap<>(); + sourceMap.put("mdstring", label); + long messageId = SharedServer.get().submitMessage(channelId, PAYLOAD, sourceMap); + Harness.awaitConnectorStatus(channelId, messageId, LAST_DESTINATION, Status.SENT); + // The statistics assertions read a channel-wide total, so wait for the commit that + // finishes the message rather than only for its last destination. + Harness.awaitProcessed(channelId, messageId); + return messageId; + } + + /** Asserts one stored raw payload is the whole message, with an attachment token per extracted value. */ + private static void assertStoredPayload(String raw, String message) { + assertTrue(raw != null && STORED_PAYLOAD.matcher(raw).matches(), () -> message + "; got " + raw); + } + + private List attachments(long messageId) throws Exception { + return SharedServer.get().fetchAttachments(channelId, messageId); + } + + /** Compares the four lifetime counters, which is what a delete can change. */ + private static void assertStatistics(ChannelStatistics expected, ChannelStatistics actual, String message) { + assertEquals( + List.of(expected.getReceived(), expected.getFiltered(), expected.getSent(), expected.getError()), + List.of(actual.getReceived(), actual.getFiltered(), actual.getSent(), actual.getError()), + () -> message + "; expected " + expected + " but got " + actual); + } +} diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/MessageStorageDisabledTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/MessageStorageDisabledTest.java new file mode 100644 index 0000000000..b85d86d151 --- /dev/null +++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/MessageStorageDisabledTest.java @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Mitch Gaffigan + +package org.openintegrationengine.smoketest; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; + +import java.util.LinkedHashMap; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import com.mirth.connect.donkey.model.message.Message; + +/** + * {@code MessageStorageMode.DISABLED}, the one storage level that ci/tests/160-message-storage-levels + * cannot express. The channel is given a pass-through DAO, so no message row is ever written and + * there is no message for the fixture harness to retrieve and assert against. + */ +@DisplayName("160-message-storage-levels/05-disabled") +@TestInstance(PER_CLASS) +class MessageStorageDisabledTest { + + private static final String CHANNEL = "channels/message-storage-disabled.xml"; + + private String channelId; + + @BeforeAll + void deploy() throws Exception { + channelId = Harness.deploy(CHANNEL); + } + + @AfterAll + void undeploy() { + Harness.undeploy(channelId); + } + + @Test + void storesNoMessage() throws Exception { + OieServer server = SharedServer.get(); + long messageId = server.submitMessage(channelId, "Hello world!", new LinkedHashMap<>()); + Message stored = server.fetchMessage(channelId, messageId); + assertNull(stored, () -> "DISABLED storage wrote a row for message " + messageId); + } +} diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/MixedDestinationStatisticsTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/MixedDestinationStatisticsTest.java new file mode 100644 index 0000000000..9909e22e50 --- /dev/null +++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/MixedDestinationStatisticsTest.java @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Mitch Gaffigan + +package org.openintegrationengine.smoketest; + +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; +import static org.openintegrationengine.smoketest.StatisticsAssertions.assertStatistics; +import static org.openintegrationengine.smoketest.StatisticsAssertions.counts; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +/** + * Destinations that end the same message differently are counted separately. Every message goes + * to all three destinations, so each destination receives the same three messages and the only + * thing that differs is the status it ends them in - which is the counter it credits. The channel + * aggregate then has to carry all three outcomes at once rather than only the last or the worst. + * + *

    + *
  • Source: a Channel Reader, which sends every message down the whole chain.
  • + *
  • Destination 1 "Filtered": a filter rule that returns false, so its messages are filtered.
  • + *
  • Destination 2 "Sent": a JavaScript Writer that returns a response.
  • + *
  • Destination 3 "Error": a JavaScript Writer that throws, with its queue off and no + * retries, so the failure is final.
  • + *
+ */ +@DisplayName("250-statistics/03-mixed-destination-outcomes") +@TestInstance(PER_CLASS) +class MixedDestinationStatisticsTest { + + private static final String CHANNEL = "channels/statistics-mixed-destinations.xml"; + + private String channelId; + + @BeforeAll + void deploy() throws Exception { + channelId = Harness.deploy(CHANNEL); + } + + @AfterAll + void undeploy() { + Harness.undeploy(channelId); + } + + @Test + void eachDestinationCreditsItsOwnOutcome() throws Exception { + for (int i = 0; i < 3; i++) { + long messageId = SharedServer.get().submitMessage(channelId, "mixed", new LinkedHashMap<>()); + Harness.awaitProcessed(channelId, messageId); + } + + assertStatistics(channelId, Map.of( + 0, counts(3, 0, 0, 0), + 1, counts(3, 3, 0, 0), + 2, counts(3, 0, 3, 0), + 3, counts(3, 0, 0, 3)), 0); + } +} diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/OieServer.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/OieServer.java index 51ded95d85..26d59a1ce5 100644 --- a/smoketest/src/test/java/org/openintegrationengine/smoketest/OieServer.java +++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/OieServer.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.util.ArrayDeque; import java.util.Deque; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -14,10 +15,15 @@ import com.mirth.connect.donkey.model.channel.DeployedState; import com.mirth.connect.donkey.model.message.Message; import com.mirth.connect.donkey.model.message.RawMessage; +import com.mirth.connect.donkey.model.message.Status; +import com.mirth.connect.donkey.model.message.attachment.Attachment; import com.mirth.connect.model.Channel; +import com.mirth.connect.model.ChannelStatistics; +import com.mirth.connect.model.DashboardStatus; import com.mirth.connect.model.LoginStatus; import com.mirth.connect.model.converters.ObjectXMLSerializer; import com.mirth.connect.model.filters.MessageFilter; +import com.mirth.connect.util.ConfigurationProperty; import com.mirth.connect.util.MirthSSLUtil; /** @@ -88,7 +94,18 @@ private static synchronized void initSerializer(String serverVersion) throws Exc * @return the deployed channel's id */ String deployChannel(String xml, String label) throws Exception { - Channel channel = ObjectXMLSerializer.getInstance().deserialize(xml, Channel.class); + return deployChannel(ObjectXMLSerializer.getInstance().deserialize(xml, Channel.class), label); + } + + /** + * Deploys a channel model, creating or overwriting whatever is on the server under its id. + * Deploying the same id twice redeploys it, which is how a test changes a channel in place. + * + * @param channel the channel to deploy + * @param label a human-readable name for the channel, used only in error messages + * @return the deployed channel's id + */ + String deployChannel(Channel channel, String label) throws Exception { String channelId = channel.getId(); if (channelId == null || channelId.isBlank()) { throw new IllegalArgumentException("Channel fixture has no id: " + label); @@ -98,7 +115,9 @@ String deployChannel(String xml, String label) throws Exception { if (!client.createChannel(channel)) { client.updateChannel(channel, true, null); } - deployedChannelIds.push(channelId); + if (!deployedChannelIds.contains(channelId)) { + deployedChannelIds.push(channelId); + } // returnErrors=true so a deploy failure surfaces here instead of only as a status // that never reaches STARTED. The String overload avoids DebuggerUtil parsing. @@ -108,20 +127,75 @@ String deployChannel(String xml, String label) throws Exception { } private void awaitStarted(String channelId, String label) throws Exception { + try { + awaitState(channelId, DeployedState.STARTED); + } catch (AssertionError e) { + throw new AssertionError("Channel " + label + " (" + channelId + ") did not start: " + e.getMessage(), e); + } + } + + private void awaitState(String channelId, DeployedState state) throws Exception { long deadline = System.nanoTime() + HarnessConfig.TIMEOUT.toNanos(); DeployedState lastState = null; while (System.nanoTime() < deadline) { - var status = client.getChannelStatus(channelId); + DashboardStatus status = client.getChannelStatus(channelId); lastState = status == null ? null : status.getState(); - if (lastState == DeployedState.STARTED) { + if (lastState == state) { return; } Thread.sleep(500); } - throw new AssertionError("Channel " + label + " (" + channelId + ") did not start within " + throw new AssertionError("Channel " + channelId + " did not reach " + state + " within " + HarnessConfig.TIMEOUT.toSeconds() + "s; last state was " + lastState); } + /** + * The number of messages queued for one connector, as the dashboard reports it: the source + * queue for metadata id 0, a destination's queue otherwise. Returns null while the channel + * has no status for that connector. + */ + Long queueSize(String channelId, int metaDataId) throws ClientException { + DashboardStatus status = client.getChannelStatus(channelId); + if (status == null) { + return null; + } + for (DashboardStatus connectorStatus : status.getChildStatuses()) { + if (Integer.valueOf(metaDataId).equals(connectorStatus.getMetaDataId())) { + return connectorStatus.getQueued(); + } + } + return Integer.valueOf(metaDataId).equals(status.getMetaDataId()) ? status.getQueued() : null; + } + + /** Stops a channel, leaving it deployed, and waits for it to report {@link DeployedState#STOPPED}. */ + void stopChannel(String channelId) throws Exception { + client.stopChannel(channelId, true); + awaitState(channelId, DeployedState.STOPPED); + } + + /** + * Halts a channel, which interrupts whatever it is processing instead of waiting for it, and + * waits for it to report {@link DeployedState#STOPPED}. + */ + void haltChannel(String channelId) throws Exception { + client.haltChannel(channelId, true); + awaitState(channelId, DeployedState.STOPPED); + } + + /** + * Starts a channel and waits for it to report {@link DeployedState#STARTED}, doing nothing if + * it is already started. Tolerating that lets a test restore a channel it stopped from a + * {@code finally} without having to know whether the stop got that far. + */ + void startChannel(String channelId) throws Exception { + DashboardStatus status = client.getChannelStatus(channelId); + if (status != null && status.getState() == DeployedState.STARTED) { + return; + } + client.startChannel(channelId, true); + awaitState(channelId, DeployedState.STARTED); + } + /** Submits a source payload and returns the new message id. */ long submitMessage(String channelId, String rawData, Map sourceMap) throws ClientException { RawMessage rawMessage = new RawMessage(rawData, null, sourceMap); @@ -132,7 +206,24 @@ long submitMessage(String channelId, String rawData, Map sourceM return messageId; } - /** Reads one message back, with content, so assertions can inspect every connector. */ + /** + * Sets one configuration map entry, leaving the rest alone. This is the only server-side + * state a client can write that channel scripts can read back, which makes it the harness's + * way to signal a running script. + * + *

The server does the read-modify-write under its own lock, so test classes running in + * parallel can set their own gate properties without dropping each other's. + */ + void setConfigurationProperty(String key, String value) throws ClientException { + client.setConfigurationProperty(key, new ConfigurationProperty(value, null)); + } + + /** + * Reads one message back, with content, so assertions can inspect every connector. Asking + * for the content switches the server's DAO out of decrypting mode, so the content is + * whatever is stored - ciphertext for a channel with {@code encryptData} set. See + * {@link #fetchDecryptedMessage}. + */ Message fetchMessage(String channelId, long messageId) throws ClientException { MessageFilter filter = new MessageFilter(); filter.setMinMessageId(messageId); @@ -145,6 +236,88 @@ Message fetchMessage(String channelId, long messageId) throws ClientException { return messages.get(0); } + /** + * Reads one message back through the decrypting path. {@link #fetchMessage} asks the server + * for the content as stored, so an encrypting channel's content arrives as ciphertext; this + * call leaves the DAO decrypting, which is what the administrator sees in the message browser. + */ + Message fetchDecryptedMessage(String channelId, long messageId, List metaDataIds) + throws ClientException { + return client.getMessageContent(channelId, messageId, metaDataIds); + } + + /** + * Every attachment stored against one message, with its content. Attachments live in their + * own table rather than on the message, so they never appear in {@link #fetchMessage}; the + * harness makes this second read only when a fixture names an {@code attachmentNN} file. + */ + List fetchAttachments(String channelId, long messageId) throws ClientException { + List attachments = client.getAttachmentsByMessageId(channelId, messageId); + return attachments == null ? List.of() : attachments; + } + + /** + * Deletes one message, or one of its connector messages when {@code metaDataId} is not null. + * This is the message browser's delete: the server turns it into a one-message filter, so + * asking for metadata id 0 deletes the whole message rather than only the source. + */ + void removeMessage(String channelId, long messageId, Integer metaDataId) throws ClientException { + client.removeMessage(channelId, messageId, metaDataId, null); + } + + /** + * Deletes every message in a channel, stopping and restarting it if it is running, and + * optionally resetting its statistics at the same time. + */ + void removeAllMessages(String channelId, boolean clearStatistics) throws ClientException { + client.removeAllMessages(channelId, true, clearStatistics); + } + + /** How many messages the channel still holds. */ + long messageCount(String channelId) throws ClientException { + Long count = client.getMessageCount(channelId, new MessageFilter()); + return count == null ? 0L : count; + } + + /** + * The channel's aggregate lifetime counters, as the dashboard's statistics view reports them. + * + *

For a deployed channel these come from the running engine's own tallies. For a channel + * that has been undeployed without being removed they come from the database, which is the + * only way a client sees what was actually stored. + */ + ChannelStatistics statistics(String channelId) throws ClientException { + return client.getStatistics(channelId); + } + + /** Undeploys a channel without removing it, so its stored rows stay where they are. */ + void undeployChannel(String channelId) throws ClientException { + client.undeployChannel(channelId, true); + } + + /** + * Every statistics counter the dashboard shows for one channel, keyed by connector: 0 for the + * source, its metadata id for each destination, and null for the channel's own aggregate row. + * Only the statuses in {@code com.mirth.connect.donkey.server.channel.Statistics}'s + * {@code TRACKED_STATUSES} are counted, so each map holds RECEIVED, FILTERED, SENT and ERROR + * and nothing else. + */ + Map> connectorStatistics(String channelId) throws ClientException { + DashboardStatus status = client.getChannelStatus(channelId); + if (status == null) { + throw new AssertionError("Channel " + channelId + " has no dashboard status"); + } + + Map> statistics = new LinkedHashMap<>(); + // The channel's own status carries the aggregate row, which the table stores under a null + // metadata id; its children carry the per-connector rows. + statistics.put(null, status.getStatistics()); + for (DashboardStatus connectorStatus : status.getChildStatuses()) { + statistics.put(connectorStatus.getMetaDataId(), connectorStatus.getStatistics()); + } + return statistics; + } + /** Undeploys and removes a channel, tolerating failures so teardown always continues. */ void removeChannel(String channelId) { deployedChannelIds.remove(channelId); diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/PersistedStatisticsTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/PersistedStatisticsTest.java new file mode 100644 index 0000000000..53b460694d --- /dev/null +++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/PersistedStatisticsTest.java @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Mitch Gaffigan + +package org.openintegrationengine.smoketest; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; +import static org.openintegrationengine.smoketest.StatisticsAssertions.assertStatistics; +import static org.openintegrationengine.smoketest.StatisticsAssertions.counts; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import com.mirth.connect.model.ChannelStatistics; + +/** + * The counters a running channel reports are held in memory and written to the database behind + * it, and every other case here reads the memory. This one reads the database. + * + *

That matters because the statement that writes them is one of the few the engine keeps a + * per-dialect version of - derby cannot use the {@code GREATEST} the others do and falls back to + * a {@code CASE} rewrite - and a write nothing reads back is a write nothing checks. A client + * only ever sees the stored rows for a channel that is not deployed, because a deployed one is + * always answered from the engine's own tallies, so the channel is undeployed without being + * removed and asked again. + * + *

The message mixture is the one from + * {@link MixedDestinationStatisticsTest}, so all four stored counters are non-zero and a write + * that lost or crossed a column has somewhere to show up. + * + *

    + *
  • Source: a Channel Reader.
  • + *
  • Destination 1 "Filtered": a filter rule that returns false.
  • + *
  • Destination 2 "Sent": a JavaScript Writer that returns a response.
  • + *
  • Destination 3 "Error": a JavaScript Writer that throws, with no queue and no retries.
  • + *
+ */ +@DisplayName("250-statistics/05-written-to-the-database") +@TestInstance(PER_CLASS) +class PersistedStatisticsTest { + + private static final String CHANNEL = "channels/statistics-persisted.xml"; + + private static final int MESSAGE_COUNT = 3; + + private String channelId; + + @BeforeAll + void deploy() throws Exception { + channelId = Harness.deploy(CHANNEL); + } + + @AfterAll + void undeploy() { + Harness.undeploy(channelId); + } + + @Test + void theStoredCountersAreTheOnesTheRunningChannelReported() throws Exception { + for (int i = 0; i < MESSAGE_COUNT; i++) { + long messageId = SharedServer.get().submitMessage(channelId, "persisted", new LinkedHashMap<>()); + Harness.awaitProcessed(channelId, messageId); + } + + assertStatistics(channelId, Map.of( + 0, counts(MESSAGE_COUNT, 0, 0, 0), + 1, counts(MESSAGE_COUNT, MESSAGE_COUNT, 0, 0), + 2, counts(MESSAGE_COUNT, 0, MESSAGE_COUNT, 0), + 3, counts(MESSAGE_COUNT, 0, 0, MESSAGE_COUNT)), 0); + + Harness.undeployChannel(channelId); + + // The engine writes the counters out on a timer, so the stored rows can be a moment + // behind the channel that has just stopped reporting them. + awaitStoredStatistics(MESSAGE_COUNT, MESSAGE_COUNT, MESSAGE_COUNT, MESSAGE_COUNT); + } + + /** Polls the stored counters until they hold what the running channel reported. */ + private void awaitStoredStatistics(long received, long filtered, long sent, long error) throws Exception { + List expected = List.of(received, filtered, sent, error); + long deadline = System.nanoTime() + HarnessConfig.TIMEOUT.toNanos(); + List actual = null; + do { + ChannelStatistics stored = SharedServer.get().statistics(channelId); + actual = List.of(stored.getReceived(), stored.getFiltered(), stored.getSent(), stored.getError()); + if (expected.equals(actual)) { + return; + } + Thread.sleep(250); + } while (System.nanoTime() < deadline); + + assertEquals(expected, actual, "the counters the engine stored are not the ones it reported"); + } +} diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/QueuedMessageDeletionTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/QueuedMessageDeletionTest.java new file mode 100644 index 0000000000..a8bba03c06 --- /dev/null +++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/QueuedMessageDeletionTest.java @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Mitch Gaffigan + +package org.openintegrationengine.smoketest; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +/** + * Deleting messages a destination queue is still holding. A queue is a database table and an + * in-memory buffer at once, so a delete that only reaches the table leaves the queue thread + * retrying rows that are no longer there; the server is supposed to invalidate the queue as part + * of the delete, which is what makes the dashboard's depth fall to zero. + * + *

The destination fails every send while the gate is shut, so the messages are on the queue and + * staying there when the delete lands, rather than racing the queue thread to be drained first. + */ +@DisplayName("240-message-deletion/02-queued-destination") +@TestInstance(PER_CLASS) +class QueuedMessageDeletionTest { + + private static final String CHANNEL = "channels/message-deletion-queued.xml"; + + /** The configuration property the destination fails on until it holds this run's token. */ + private static final String GATE = "oie.smoketest.releaseDeletionQueue"; + + private static final int DESTINATION = 1; + + private static final int MESSAGE_COUNT = 4; + + private String channelId; + + @BeforeAll + void deploy() throws Exception { + channelId = Harness.deploy(CHANNEL); + } + + @AfterAll + void undeploy() { + Harness.undeploy(channelId); + } + + @Test + void deletingQueuedMessagesEmptiesTheQueue() throws Exception { + OieServer server = SharedServer.get(); + // The payload carries this run's token, so a gate left open by an earlier run cannot + // release this one. + String token = UUID.randomUUID().toString(); + List messageIds = new ArrayList<>(); + + try { + for (int i = 1; i <= MESSAGE_COUNT; i++) { + messageIds.add(server.submitMessage(channelId, token, new LinkedHashMap<>())); + } + Harness.awaitQueueSizeAtLeast(channelId, DESTINATION, MESSAGE_COUNT); + + for (long messageId : messageIds) { + server.removeMessage(channelId, messageId, null); + } + + assertEquals(0, server.messageCount(channelId), "the queued messages were not deleted"); + Harness.awaitQueueSizeAtMost(channelId, DESTINATION, 0); + } finally { + // Also on failure, so a channel left behind by this test is not a wedged one. + Harness.setConfigurationProperty(GATE, token); + } + } +} diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/SourceFilteredStatisticsTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/SourceFilteredStatisticsTest.java new file mode 100644 index 0000000000..da2c19254e --- /dev/null +++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/SourceFilteredStatisticsTest.java @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Mitch Gaffigan + +package org.openintegrationengine.smoketest; + +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; +import static org.openintegrationengine.smoketest.StatisticsAssertions.assertStatistics; +import static org.openintegrationengine.smoketest.StatisticsAssertions.counts; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +/** + * What a channel counts when its source filter rejects everything. A message the source filters + * never reaches a destination, so the destination's counters have to stay at zero while the + * channel still counts the message as both received and filtered - RECEIVED is what came in, not + * what survived, and the two are separate counters rather than one moving between them. + * + *

    + *
  • Source: a Channel Reader with a filter rule that returns false for every message.
  • + *
  • Destination 1: a JavaScript Writer that would send, and never gets the chance.
  • + *
+ */ +@DisplayName("250-statistics/02-filtered-at-the-source") +@TestInstance(PER_CLASS) +class SourceFilteredStatisticsTest { + + private static final String CHANNEL = "channels/statistics-source-filtered.xml"; + + private String channelId; + + @BeforeAll + void deploy() throws Exception { + channelId = Harness.deploy(CHANNEL); + } + + @AfterAll + void undeploy() { + Harness.undeploy(channelId); + } + + @Test + void aMessageFilteredAtTheSourceIsCountedThereAndNowhereElse() throws Exception { + for (int i = 0; i < 3; i++) { + long messageId = SharedServer.get().submitMessage(channelId, "rejected", new LinkedHashMap<>()); + Harness.awaitProcessed(channelId, messageId); + } + + assertStatistics(channelId, Map.of( + 0, counts(3, 3, 0, 0), + 1, counts(0, 0, 0, 0)), 0); + } +} diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/SourceQueueTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/SourceQueueTest.java new file mode 100644 index 0000000000..d91bb610d1 --- /dev/null +++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/SourceQueueTest.java @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Mitch Gaffigan + +package org.openintegrationengine.smoketest; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import com.mirth.connect.donkey.model.message.ConnectorMessage; +import com.mirth.connect.donkey.model.message.Status; + +/** + * The source queue: that it keeps its order once it is deeper than the buffer it holds in + * memory, that it survives the channel it belongs to being halted, and that concurrent + * submitters cannot lose a message between committing it and queueing it. + * + *

The channel sets {@code queueBufferSize} to 3 and every test here queues more than that, + * so refilling the buffer from the database is the path most messages take rather than an edge + * case. Its destination parks until the gate property holds the run's token, which is what lets + * a queue build up before anything drains; the token is fresh per run, so a gate left open by an + * earlier run cannot release a later one. + * + *

The destination stamps each message with the position it was handed over in, taken from a + * counter in the global channel map. Assertions are on the relative positions - a + * consecutive ascending run - because the counter carries across tests and resets when the + * channel restarts, and because relative positions are exactly what the ordering claim is. + */ +@DisplayName("220-queueing/01-source-queue") +@TestInstance(PER_CLASS) +class SourceQueueTest { + + private static final String CHANNEL = "channels/source-queue-order.xml"; + + /** The configuration property the destination parks on. */ + private static final String GATE = "oie.smoketest.releaseSourceQueue"; + + /** What the destination prefixes its handover position with. */ + private static final String POSITION = "order="; + + private static final int SOURCE = 0; + private static final int DESTINATION = 1; + + /** {@code queueBufferSize} in the channel: how much of the source queue is held in memory. */ + private static final int BUFFER_CAPACITY = 3; + + private static final int MESSAGE_COUNT = 10; + + private String channelId; + + @BeforeAll + void deploy() throws Exception { + channelId = Harness.deploy(CHANNEL); + } + + @AfterAll + void undeploy() { + Harness.undeploy(channelId); + } + + /** + * A queue deeper than its buffer still hands every message to the destination exactly once + * and in the order they were received, which means the refill from the database picks up + * where the buffer left off rather than starting over or skipping. + */ + @Test + void drainsInReceiptOrderPastItsBufferCapacity() throws Exception { + String token = UUID.randomUUID().toString(); + List messageIds = new ArrayList<>(); + + try { + for (int i = 1; i <= MESSAGE_COUNT; i++) { + messageIds.add(SharedServer.get().submitMessage(channelId, token + "#" + i, new LinkedHashMap<>())); + } + + // The first message is parked in the destination and the rest are behind it. Waiting + // for the queue to pass its buffer is what makes the refill certain rather than hoped + // for; if it never happens the wait fails the test. + Harness.awaitQueueSizeAtLeast(channelId, SOURCE, BUFFER_CAPACITY + 1); + } finally { + // Also on failure, so a parked destination never outlives the test. + Harness.setConfigurationProperty(GATE, token); + } + + assertConsecutive(handoverPositions(messageIds), + "the source queue should drain in the order the messages were received"); + } + + /** + * Halting a channel with a full source queue leaves the queue alone, and starting it again + * drains it in order. A halt rather than a stop because a stop waits for the message the + * destination is parked on, which is the one thing this test needs to still be parked. + * + *

That the queue thread is not running in between is not asserted - it cannot be, over an + * API that only reports state. What is asserted is the consequence that matters: the queue + * was still there afterwards, and the restarted channel reloaded and drained it. + */ + @Test + void survivesAHaltedChannelAndDrainsWhenItStartsAgain() throws Exception { + String token = UUID.randomUUID().toString(); + List messageIds = new ArrayList<>(); + + try { + for (int i = 1; i <= MESSAGE_COUNT; i++) { + messageIds.add(SharedServer.get().submitMessage(channelId, token + "#" + i, new LinkedHashMap<>())); + } + // One message is in the destination, so the rest are the queue. + int queued = MESSAGE_COUNT - 1; + Harness.awaitQueueSizeAtLeast(channelId, SOURCE, queued); + + Harness.haltChannel(channelId); + + assertEquals(Long.valueOf(queued), SharedServer.get().queueSize(channelId, SOURCE), + "halting the channel should leave its source queue intact, not drain or drop it"); + } finally { + // Also on failure, so neither a parked destination nor a halted channel outlives the + // test. Starting an already-started channel is a no-op, so this is safe either way. + Harness.setConfigurationProperty(GATE, token); + Harness.startChannel(channelId); + } + + assertConsecutive(handoverPositions(messageIds), + "the restarted channel should drain the queue it reloaded, in order"); + } + + /** + * Several connections dispatching at once still get every message processed exactly once. + * The engine commits a source message and adds it to the queue under the queue's own lock; + * without that, a message committed but not yet queued is picked up by another thread's + * buffer refill and the queue's count and contents disagree, stranding one of them. + * {@code ConnectorMessageQueueTest} pins that mechanism down directly - this is the same + * claim where a client can see it, so it catches the lock going missing rather than + * describing what happens when it does. + * + *

A connection serialises its own requests, so each submitter needs its own; sharing one + * would serialise the very dispatch this is about. + */ + @Test + void concurrentSubmittersEachGetTheirMessageProcessedExactlyOnce() throws Exception { + String token = UUID.randomUUID().toString(); + int submitters = 3; + int perSubmitter = 8; + + List messageIds = new ArrayList<>(); + List connections = new ArrayList<>(); + ExecutorService submitterPool = Executors.newFixedThreadPool(submitters); + + try { + List>> submitted = new ArrayList<>(); + for (int submitter = 0; submitter < submitters; submitter++) { + OieServer connection = OieServer.connect(); + connections.add(connection); + + String prefix = token + "#" + submitter + "-"; + submitted.add(submitterPool.submit(() -> { + List mine = new ArrayList<>(); + for (int i = 1; i <= perSubmitter; i++) { + mine.add(connection.submitMessage(channelId, prefix + i, new LinkedHashMap<>())); + } + return mine; + })); + } + for (Future> batch : submitted) { + messageIds.addAll(batch.get()); + } + + Harness.awaitQueueSizeAtLeast(channelId, SOURCE, BUFFER_CAPACITY + 1); + } finally { + Harness.setConfigurationProperty(GATE, token); + submitterPool.shutdown(); + connections.forEach(OieServer::close); + } + + // Which submitter won a given position is a race and not asserted; that every message got + // a position, and no two got the same one, is the claim. + List positions = handoverPositions(messageIds); + Collections.sort(positions); + assertEquals(submitters * perSubmitter, positions.size(), "a message went missing"); + assertConsecutive(positions, "every concurrently submitted message should be handed over" + + " exactly once, so their positions are distinct and leave no gap"); + } + + /** + * Waits for every message to be sent and returns the position its destination stamped it + * with, in the order the server committed them - which is message id order, since the id is + * assigned by the insert. + */ + private List handoverPositions(List messageIds) throws Exception { + List inCommitOrder = new ArrayList<>(messageIds); + Collections.sort(inCommitOrder); + + List positions = new ArrayList<>(); + for (long messageId : inCommitOrder) { + ConnectorMessage sent = Harness.awaitConnectorStatus(channelId, messageId, DESTINATION, Status.SENT); + String stamp = MessageAssertions.responsePayload(sent.getResponse().getContent()); + assertTrue(stamp != null && stamp.startsWith(POSITION), + "message " + messageId + " has an unexpected destination response: " + stamp); + positions.add(Integer.valueOf(stamp.substring(POSITION.length()))); + } + return positions; + } + + /** + * Asserts the positions are a consecutive ascending run. Where they start is the channel + * counter's business; that they rise by one each time is the queue's. + */ + private static void assertConsecutive(List positions, String message) { + List expected = new ArrayList<>(); + for (int i = 0; i < positions.size(); i++) { + expected.add(positions.get(0) + i); + } + assertEquals(expected, positions, message); + } +} diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/StalledDestinationStatisticsTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/StalledDestinationStatisticsTest.java new file mode 100644 index 0000000000..01903c71a6 --- /dev/null +++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/StalledDestinationStatisticsTest.java @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Mitch Gaffigan + +package org.openintegrationengine.smoketest; + +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; +import static org.openintegrationengine.smoketest.StatisticsAssertions.assertStatistics; +import static org.openintegrationengine.smoketest.StatisticsAssertions.counts; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +/** + * What a channel reports while a destination is stalled. A queued message has been received by + * the destination but not sent, and the queue it is sitting on is a depth the server measures + * rather than a counter it keeps: the destination's RECEIVED rises as soon as the message reaches + * it, SENT stays where it was until the destination actually accepts the message, and the queue + * depth is what carries the difference in the meantime. + * + *

    + *
  • Source: a Channel Reader.
  • + *
  • Destination 1: a JavaScript Writer with its queue on, which returns QUEUED - a send failure + * the queue retries, not an error - until the configuration map holds this run's token.
  • + *
+ */ +@DisplayName("250-statistics/04-stalled-destination") +@TestInstance(PER_CLASS) +class StalledDestinationStatisticsTest { + + private static final String CHANNEL = "channels/statistics-queued-destination.xml"; + + /** The configuration property the destination stalls on until it holds this run's token. */ + private static final String GATE = "oie.smoketest.releaseStatisticsQueue"; + + private static final int DESTINATION = 1; + + private static final int MESSAGE_COUNT = 3; + + private String channelId; + + @BeforeAll + void deploy() throws Exception { + channelId = Harness.deploy(CHANNEL); + } + + @AfterAll + void undeploy() { + Harness.undeploy(channelId); + } + + @Test + void aStalledDestinationReportsItsQueueDepthUntilItSends() throws Exception { + // The payload is this run's token, so a gate left open by an earlier run cannot release + // this one, and every message carries the same token so one write releases them all. + String token = UUID.randomUUID().toString(); + + try { + for (int i = 0; i < MESSAGE_COUNT; i++) { + SharedServer.get().submitMessage(channelId, token, new LinkedHashMap<>()); + } + // The depth is a precondition the test enforces rather than one it hopes for: nothing + // below is read until the server itself says all three messages are on the queue. + Harness.awaitQueueSizeAtLeast(channelId, DESTINATION, MESSAGE_COUNT); + + assertStatistics(channelId, Map.of( + 0, counts(MESSAGE_COUNT, 0, 0, 0), + DESTINATION, counts(MESSAGE_COUNT, 0, 0, 0)), MESSAGE_COUNT); + } finally { + // Also on failure, so a channel left behind by this test is not a wedged one. + Harness.setConfigurationProperty(GATE, token); + } + + Harness.awaitQueueSizeAtMost(channelId, DESTINATION, 0); + + assertStatistics(channelId, Map.of( + 0, counts(MESSAGE_COUNT, 0, 0, 0), + DESTINATION, counts(MESSAGE_COUNT, 0, MESSAGE_COUNT, 0)), 0); + } +} diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/StatisticsAccountingTest.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/StatisticsAccountingTest.java new file mode 100644 index 0000000000..455aac7bfa --- /dev/null +++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/StatisticsAccountingTest.java @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Mitch Gaffigan + +package org.openintegrationengine.smoketest; + +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; +import static org.openintegrationengine.smoketest.StatisticsAssertions.assertStatistics; +import static org.openintegrationengine.smoketest.StatisticsAssertions.counts; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +/** + * What a channel counts when nothing goes wrong. The channel has two destinations that both + * accept every message, so the source and each destination keep their own counters and the + * channel's aggregate SENT is the two destinations added together rather than either one of them. + * + *

Statistics are a channel-wide running total rather than a property of a message, so there is + * no fixture shape for them; every case under {@code 250-statistics} reads them back over the + * client API instead. + * + *

    + *
  • Source: a Channel Reader, so the harness submits the messages itself.
  • + *
  • Destinations 1 and 2: JavaScript Writers that return a response, which is a send.
  • + *
+ */ +@DisplayName("250-statistics/01-normal-flow") +@TestInstance(PER_CLASS) +class StatisticsAccountingTest { + + private static final String CHANNEL = "channels/statistics-normal-flow.xml"; + + private String channelId; + + @BeforeAll + void deploy() throws Exception { + channelId = Harness.deploy(CHANNEL); + } + + @AfterAll + void undeploy() { + Harness.undeploy(channelId); + } + + @Test + void everyConnectorCountsTheMessagesItHandled() throws Exception { + submit(); + + // Asserted after one message and again after three, so a counter that was assigned the + // message count rather than incremented per message would still have to be caught. + assertStatistics(channelId, Map.of( + 0, counts(1, 0, 0, 0), + 1, counts(1, 0, 1, 0), + 2, counts(1, 0, 1, 0)), 0); + + submit(); + submit(); + + assertStatistics(channelId, Map.of( + 0, counts(3, 0, 0, 0), + 1, counts(3, 0, 3, 0), + 2, counts(3, 0, 3, 0)), 0); + } + + /** Submits one message and waits for the server to finish with it, counters included. */ + private void submit() throws Exception { + long messageId = SharedServer.get().submitMessage(channelId, "counted", new LinkedHashMap<>()); + Harness.awaitProcessed(channelId, messageId); + } +} diff --git a/smoketest/src/test/java/org/openintegrationengine/smoketest/StatisticsAssertions.java b/smoketest/src/test/java/org/openintegrationengine/smoketest/StatisticsAssertions.java new file mode 100644 index 0000000000..791a502556 --- /dev/null +++ b/smoketest/src/test/java/org/openintegrationengine/smoketest/StatisticsAssertions.java @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Mitch Gaffigan + +package org.openintegrationengine.smoketest; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.Duration; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.mirth.connect.donkey.model.message.Status; +import com.mirth.connect.model.ChannelStatistics; + +/** + * Statistics assertions shared by the cases under {@code 250-statistics}. + * + *

A channel's counters reach a client two ways, computed separately by the server. The + * dashboard reports a row per connector plus the channel's own aggregate row, which the engine + * maintains as it goes; the statistics view reports one {@link ChannelStatistics} the server + * re-adds up from the connector rows on each call. A test here states the per-connector counters + * it expects and {@link #assertStatistics} derives both aggregates from them, so each case pins + * down the connector counters it is about and every case cross-checks the two aggregates against + * the same rule: + * + *

    + *
  • RECEIVED counts the source connector only.
  • + *
  • SENT counts the destination connectors only.
  • + *
  • FILTERED and ERROR count every connector.
  • + *
+ * + *

Only the four statuses in + * {@code com.mirth.connect.donkey.server.channel.Statistics.TRACKED_STATUSES} are counted at all; + * a queue is a depth the server measures when asked rather than a counter, which is why it is a + * separate argument. + */ +final class StatisticsAssertions { + + /** + * How long to keep re-reading before giving up on a counter. A connector's status and its + * counters are committed together but read back separately, so a caller that waited for a + * status can still be a moment early for the counter it implies. + */ + private static final Duration GRACE = Duration.ofSeconds(5); + + private static final Duration POLL_INTERVAL = Duration.ofMillis(250); + + private StatisticsAssertions() { + } + + /** One connector's counters, in the order the engine tracks them. */ + static Map counts(long received, long filtered, long sent, long error) { + Map counts = new LinkedHashMap<>(); + counts.put(Status.RECEIVED, received); + counts.put(Status.FILTERED, filtered); + counts.put(Status.SENT, sent); + counts.put(Status.ERROR, error); + return counts; + } + + /** + * Asserts every counter the server reports for one channel: the per-connector rows against + * {@code expected}, keyed by metadata id, and both aggregates against the roll-up of + * {@code expected} described above. Retries for {@link #GRACE} before failing. + * + * @param expectedQueued how many messages should be sitting on the channel's queues + */ + static void assertStatistics(String channelId, Map> expected, long expectedQueued) + throws Exception { + long deadline = System.nanoTime() + GRACE.toNanos(); + while (true) { + try { + assertStatisticsNow(channelId, expected, expectedQueued); + return; + } catch (AssertionError e) { + if (System.nanoTime() >= deadline) { + throw e; + } + } + Thread.sleep(POLL_INTERVAL.toMillis()); + } + } + + private static void assertStatisticsNow(String channelId, Map> expected, + long expectedQueued) throws Exception { + Map> actual = SharedServer.get().connectorStatistics(channelId); + + Map> actualConnectors = new LinkedHashMap<>(actual); + Map actualAggregate = actualConnectors.remove(null); + assertEquals(expected, actualConnectors, "the per-connector statistics are wrong"); + + Map expectedAggregate = rollUp(expected); + assertEquals(expectedAggregate, actualAggregate, "the channel's aggregate statistics row is wrong"); + + // The statistics view adds the connector rows up again on its own, so it has to agree. + ChannelStatistics statistics = SharedServer.get().statistics(channelId); + assertEquals( + List.of(expectedAggregate.get(Status.RECEIVED), expectedAggregate.get(Status.FILTERED), + expectedAggregate.get(Status.SENT), expectedAggregate.get(Status.ERROR), expectedQueued), + List.of(statistics.getReceived(), statistics.getFiltered(), statistics.getSent(), + statistics.getError(), statistics.getQueued()), + () -> "the statistics view disagrees with the dashboard; it reported " + statistics); + } + + /** Adds the per-connector counters up the way the channel's own aggregate is defined. */ + private static Map rollUp(Map> connectors) { + long received = 0; + long filtered = 0; + long sent = 0; + long error = 0; + + for (Map.Entry> connector : connectors.entrySet()) { + boolean source = connector.getKey() == 0; + Map counts = connector.getValue(); + received += source ? counts.get(Status.RECEIVED) : 0; + sent += source ? 0 : counts.get(Status.SENT); + filtered += counts.get(Status.FILTERED); + error += counts.get(Status.ERROR); + } + + return counts(received, filtered, sent, error); + } +} diff --git a/smoketest/src/test/resources/channels/blocking-response-transformer.xml b/smoketest/src/test/resources/channels/blocking-response-transformer.xml new file mode 100644 index 0000000000..8c800757d0 --- /dev/null +++ b/smoketest/src/test/resources/channels/blocking-response-transformer.xml @@ -0,0 +1,199 @@ + + 6a9c1b80-2d34-4f57-8a61-93b7c05de266 + 2 + blocking-response-transformer + + 1 + + 0 + sourceConnector + + + + None + false + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Wait for the gate + 0 + true + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + JavaScript Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/smoketest/src/test/resources/channels/custom-metadata-columns-redeploy.xml b/smoketest/src/test/resources/channels/custom-metadata-columns-redeploy.xml new file mode 100644 index 0000000000..3794c5ceb0 --- /dev/null +++ b/smoketest/src/test/resources/channels/custom-metadata-columns-redeploy.xml @@ -0,0 +1,189 @@ + + 7c1f0a94-2d63-4b85-9e17-0a5d3b6c2002 + 2 + custom-metadata-columns-redeploy + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + KEPT + STRING + kept + + + DROPPED + STRING + dropped + + + RETYPED + STRING + retyped + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/smoketest/src/test/resources/channels/destination-queue-rotate.xml b/smoketest/src/test/resources/channels/destination-queue-rotate.xml new file mode 100644 index 0000000000..92bbf77c52 --- /dev/null +++ b/smoketest/src/test/resources/channels/destination-queue-rotate.xml @@ -0,0 +1,197 @@ + + beb47186-5e12-4300-8fdc-969ce73ea22f + 2 + destination-queue-rotate + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + true + false + 100 + false + 0 + true + false + 1 + + false + + + Default Resource + [Default Resource] + + + 3 + true + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + JavaScript Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/smoketest/src/test/resources/channels/destination-queue.xml b/smoketest/src/test/resources/channels/destination-queue.xml new file mode 100644 index 0000000000..2446e52335 --- /dev/null +++ b/smoketest/src/test/resources/channels/destination-queue.xml @@ -0,0 +1,195 @@ + + 95d19768-faf7-41d0-9cb6-e32bd47b8bba + 2 + destination-queue + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + true + false + 100 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 3 + true + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + JavaScript Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/smoketest/src/test/resources/channels/encryption-at-rest.xml b/smoketest/src/test/resources/channels/encryption-at-rest.xml new file mode 100644 index 0000000000..765376cb75 --- /dev/null +++ b/smoketest/src/test/resources/channels/encryption-at-rest.xml @@ -0,0 +1,191 @@ + + bfeb4c3b-d1f5-4c31-9645-947a70411175 + 2 + encryption-at-rest + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + + Prefix the message + 0 + true + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + true + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/smoketest/src/test/resources/channels/message-deletion-queued.xml b/smoketest/src/test/resources/channels/message-deletion-queued.xml new file mode 100644 index 0000000000..013616a2a4 --- /dev/null +++ b/smoketest/src/test/resources/channels/message-deletion-queued.xml @@ -0,0 +1,190 @@ + + c7b3a512-9d64-4f28-8a10-5e3b6c910402 + 2 + message-deletion-queued + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + true + false + 100 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 3 + true + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + JavaScript Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/smoketest/src/test/resources/channels/message-deletion.xml b/smoketest/src/test/resources/channels/message-deletion.xml new file mode 100644 index 0000000000..2f72cc37ec --- /dev/null +++ b/smoketest/src/test/resources/channels/message-deletion.xml @@ -0,0 +1,275 @@ + + c7b3a512-9d64-4f28-8a10-5e3b6c910401 + 3 + Message Deletion + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + + No-op + 0 + true + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Writer + DESTINATION + true + true + + + 2 + Destination 2 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + MDSTRING + STRING + mdstring + + + + com.mirth.connect.server.attachments.regex.RegexAttachmentHandlerProvider + Regex + + + regex.pattern0 + PHOTO:(.*?); + + + regex.mimetype0 + image/jpeg + + + regex.pattern1 + REPORT:(.*?); + + + regex.mimetype1 + text/plain + + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/smoketest/src/test/resources/channels/message-storage-disabled.xml b/smoketest/src/test/resources/channels/message-storage-disabled.xml new file mode 100644 index 0000000000..8f720a2ba7 --- /dev/null +++ b/smoketest/src/test/resources/channels/message-storage-disabled.xml @@ -0,0 +1,191 @@ + + 8f1d2c40-1a11-4a1e-9f9b-2c0a5a4e1605 + 2 + message-storage-disabled + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + + Prefix the message + 0 + true + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + none + ${message.encodedData} + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DISABLED + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/smoketest/src/test/resources/channels/source-queue-order.xml b/smoketest/src/test/resources/channels/source-queue-order.xml new file mode 100644 index 0000000000..0522f8527b --- /dev/null +++ b/smoketest/src/test/resources/channels/source-queue-order.xml @@ -0,0 +1,198 @@ + + a8f6f095-5dd0-43f5-be90-1d8fed13ebb7 + 2 + source-queue-order + + 1 + + 0 + sourceConnector + + + + None + false + false + false + 1 + + + Default Resource + [Default Resource] + + + 3 + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 10000 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + JavaScript Writer + DESTINATION + true + true + + + // Modify the message variable below to pre process data +return message; + // This script executes once after a message has been processed +// Responses returned from here will be stored as "Postprocessor" in the response map +return; + // This script executes once when the channel is deployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + // This script executes once when the channel is undeployed +// You only have access to the globalMap and globalChannelMap here to persist data +return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + \ No newline at end of file diff --git a/smoketest/src/test/resources/channels/statistics-mixed-destinations.xml b/smoketest/src/test/resources/channels/statistics-mixed-destinations.xml new file mode 100644 index 0000000000..9c2656428b --- /dev/null +++ b/smoketest/src/test/resources/channels/statistics-mixed-destinations.xml @@ -0,0 +1,321 @@ + + 1d4c0f3a-5c1e-4a2b-9f60-2b7c6a1d8e03 + 4 + statistics-mixed-destinations + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Filtered + + + + false + false + 100 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Reject every message + 0 + true + NONE + + + + + JavaScript Writer + DESTINATION + true + true + + + 2 + Sent + + + + false + false + 100 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + JavaScript Writer + DESTINATION + true + true + + + 3 + Error + + + + false + false + 100 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + JavaScript Writer + DESTINATION + true + true + + + return message; + return; + return; + return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + diff --git a/smoketest/src/test/resources/channels/statistics-normal-flow.xml b/smoketest/src/test/resources/channels/statistics-normal-flow.xml new file mode 100644 index 0000000000..44887af3eb --- /dev/null +++ b/smoketest/src/test/resources/channels/statistics-normal-flow.xml @@ -0,0 +1,244 @@ + + 1d4c0f3a-5c1e-4a2b-9f60-2b7c6a1d8e01 + 3 + statistics-normal-flow + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 100 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + JavaScript Writer + DESTINATION + true + true + + + 2 + Destination 2 + + + + false + false + 100 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + JavaScript Writer + DESTINATION + true + true + + + return message; + return; + return; + return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + diff --git a/smoketest/src/test/resources/channels/statistics-persisted.xml b/smoketest/src/test/resources/channels/statistics-persisted.xml new file mode 100644 index 0000000000..13a132a834 --- /dev/null +++ b/smoketest/src/test/resources/channels/statistics-persisted.xml @@ -0,0 +1,321 @@ + + 1d4c0f3a-5c1e-4a2b-9f60-2b7c6a1d8e05 + 4 + statistics-persisted + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Filtered + + + + false + false + 100 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Reject every message + 0 + true + NONE + + + + + JavaScript Writer + DESTINATION + true + true + + + 2 + Sent + + + + false + false + 100 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + JavaScript Writer + DESTINATION + true + true + + + 3 + Error + + + + false + false + 100 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + JavaScript Writer + DESTINATION + true + true + + + return message; + return; + return; + return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + diff --git a/smoketest/src/test/resources/channels/statistics-queued-destination.xml b/smoketest/src/test/resources/channels/statistics-queued-destination.xml new file mode 100644 index 0000000000..105f21e4c4 --- /dev/null +++ b/smoketest/src/test/resources/channels/statistics-queued-destination.xml @@ -0,0 +1,182 @@ + + 1d4c0f3a-5c1e-4a2b-9f60-2b7c6a1d8e04 + 2 + statistics-queued-destination + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + true + false + 100 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + JavaScript Writer + DESTINATION + true + true + + + return message; + return; + return; + return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + + diff --git a/smoketest/src/test/resources/channels/statistics-source-filtered.xml b/smoketest/src/test/resources/channels/statistics-source-filtered.xml new file mode 100644 index 0000000000..d52a972428 --- /dev/null +++ b/smoketest/src/test/resources/channels/statistics-source-filtered.xml @@ -0,0 +1,183 @@ + + 1d4c0f3a-5c1e-4a2b-9f60-2b7c6a1d8e02 + 2 + statistics-source-filtered + + 1 + + 0 + sourceConnector + + + + None + true + false + false + 1 + + + Default Resource + [Default Resource] + + + 1000 + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + Reject every message + 0 + true + NONE + + + + + Channel Reader + SOURCE + true + true + + + + 1 + Destination 1 + + + + false + false + 100 + false + 0 + false + false + 1 + + false + + + Default Resource + [Default Resource] + + + 1000 + true + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + RAW + RAW + + + JavaScript + + + + + + JavaScript + + + + + + + + JavaScript Writer + DESTINATION + true + true + + + return message; + return; + return; + return; + + true + DEVELOPMENT + false + false + false + false + false + false + STARTED + true + + + SOURCE + STRING + mirth_source + + + TYPE + STRING + mirth_type + + + + None + + + + + Default Resource + [Default Resource] + + + + + + true + + + America/Chicago + + + true + false + + 1 + + +