├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── automerge.yml │ ├── build.yml │ └── deploy.yml ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── antora.yml └── modules │ └── ROOT │ ├── nav.adoc │ └── pages │ └── index.adoc ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── it └── spring_boot │ ├── pom.xml │ └── src │ ├── main │ └── java │ │ └── com │ │ └── github │ │ └── fridujo │ │ └── rabbitmq │ │ └── mock │ │ └── integration │ │ └── springboot │ │ ├── AmqpApplication.java │ │ ├── Receiver.java │ │ └── Sender.java │ └── test │ └── java │ └── com │ └── github │ └── fridujo │ └── rabbitmq │ └── mock │ └── integration │ └── springboot │ ├── AmqpApplicationTest.java │ └── AmqpApplicationTestConfiguration.java ├── main └── java │ └── com │ └── github │ └── fridujo │ └── rabbitmq │ └── mock │ ├── AmqArguments.java │ ├── AmqpExceptions.java │ ├── ConfigurableConnectionFactory.java │ ├── ConfirmListenerWrapper.java │ ├── ConsumerWrapper.java │ ├── DeadLettering.java │ ├── Message.java │ ├── MessageComparator.java │ ├── MockChannel.java │ ├── MockConnection.java │ ├── MockConnectionFactory.java │ ├── MockNode.java │ ├── MockQueue.java │ ├── RandomStringGenerator.java │ ├── Receiver.java │ ├── ReceiverPointer.java │ ├── ReceiverRegistry.java │ ├── Transaction.java │ ├── TransactionalOperations.java │ ├── compatibility │ ├── MockConnectionFactoryFactory.java │ └── MockConnectionFactoryWithoutAddressResolver.java │ ├── configuration │ ├── Configuration.java │ └── QueueDeclarator.java │ ├── exchange │ ├── BindableMockExchange.java │ ├── ConsistentHashExchange.java │ ├── MockDefaultExchange.java │ ├── MockDirectExchange.java │ ├── MockExchange.java │ ├── MockExchangeCreator.java │ ├── MockExchangeFactory.java │ ├── MockFanoutExchange.java │ ├── MockHeadersExchange.java │ ├── MockTopicExchange.java │ ├── MultipleReceiverExchange.java │ ├── SingleReceiverExchange.java │ └── TypedMockExchangeCreator.java │ ├── metrics │ ├── ImplementedMetricsCollectorWrapper.java │ ├── MetricsCollectorWrapper.java │ └── NoopMetricsCollectorWrapper.java │ └── tool │ ├── Classes.java │ ├── Exceptions.java │ ├── NamedThreadFactory.java │ └── RestartableExecutorService.java └── test ├── java └── com │ └── github │ └── fridujo │ └── rabbitmq │ └── mock │ ├── ChannelTest.java │ ├── ComplexUseCasesTests.java │ ├── ExtensionTest.java │ ├── IntegrationTest.java │ ├── MetricsCollectorTest.java │ ├── MockConnectionFactoryTest.java │ ├── MockConnectionTest.java │ ├── RandomStringGeneratorTest.java │ ├── compatibility │ └── MockConnectionFactoryFactoryTest.java │ ├── exchange │ ├── ConsistentHashExchangeTests.java │ ├── ExchangeTest.java │ └── FixDelayExchange.java │ ├── metrics │ └── MetricsCollectorWrapperTest.java │ ├── spring │ └── SpringIntegrationTest.java │ └── tool │ ├── ClassesTest.java │ ├── ExceptionsTest.java │ ├── RestartableExecutorServiceTest.java │ └── SafeArgumentMatchers.java └── resources └── logback-test.xml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | charset = utf-8 8 | 9 | indent_style = space 10 | indent_size = 4 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request_target 3 | 4 | permissions: 5 | pull-requests: write 6 | contents: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Enable auto-merge for Dependabot PRs 14 | run: gh pr merge --auto --rebase "$PR_URL" 15 | env: 16 | PR_URL: ${{github.event.pull_request.html_url}} 17 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | name: Build 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-java@v3 12 | with: 13 | distribution: 'temurin' 14 | java-version: '17' 15 | cache: 'maven' 16 | - run: ./mvnw -version 17 | - run: ./mvnw -U verify -DfailIfNoTests 18 | - uses: actions/upload-artifact@v3 19 | if: failure() 20 | with: 21 | name: it-logs 22 | path: target/it/**/build.log 23 | - uses: codecov/codecov-action@v3 24 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to OSSRH 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-java@v3 14 | with: 15 | distribution: 'temurin' 16 | java-version: '17' 17 | server-id: ossrh 18 | server-username: MAVEN_USERNAME 19 | server-password: MAVEN_PASSWORD 20 | gpg-private-key: ${{ secrets.OSSRH_GPG_SECRET_KEY }} 21 | gpg-passphrase: MAVEN_GPG_PASSPHRASE 22 | - name: 🚀 Deploy artifact 23 | run: | 24 | mvn help:effective-settings 25 | mvn -B --no-transfer-progress -DskipTests -DperformRelease=true deploy 26 | env: 27 | MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} 28 | MAVEN_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} 29 | MAVEN_GPG_PASSPHRASE: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Maven 2 | target/ 3 | pom.xml.tag 4 | pom.xml.releaseBackup 5 | pom.xml.versionsBackup 6 | pom.xml.next 7 | release.properties 8 | dependency-reduced-pom.xml 9 | buildNumber.properties 10 | .mvn/timing.properties 11 | 12 | # Ignoring Maven wrapper to be downloaded 13 | .mvn/wrapper/maven-wrapper.jar 14 | 15 | 16 | # IntelliJ 17 | .idea/ 18 | *.iml 19 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to RabbitMQ-mock 2 | 3 | :+1::tada: First off, thanks for taking the time to contribute! :tada::+1: 4 | 5 | ## How Can I Contribute? 6 | Know that I will try my best to help you, whatever path you choose 7 | 8 | ### Reporting Bugs 9 | Using *proper English*, explain the problem and include additional details like links to the official documentation of RabbitMQ. 10 | Then describe a reproducible scenario, that can be directly turned into a **runnable test**. 11 | Better, provide some code, inlined in the issue or in a dedicated repository showing the abnormal behavior. 12 | 13 | *In fine*, the fix **will** be shipped with tests proving that it works (~better than before). 14 | 15 | ### Suggesting a new Feature 16 | The sole purpose of this project is to ease the life of Java-ish (understand, any language on the JVM) developpers working with RabbitMQ. 17 | So start to explain how this feature will improve the developer experience (reducing boiler-code, speeding-up feedback loop, etc.). 18 | If there is a new API involved, please submit your idea about it, whatever language (JVM based maybe ?). 19 | 20 | ### Pull Requests 21 | When writing a PR, please take the time to read the existing code, and follow the implicit formatting rules (brace on the same line than **if** statement, this kind of stuff). 22 | Plus, supply one or more tests covering the production code changes. 23 | Avoid if possible, mutable structures (especially unused-and-without-control *setters*), this projects serves in multi-threaded contexts and it must be modified avoiding race-conditions or non-deterministic behaviors. 24 | Last but not least, take good care of your Git history, PR are integrated using **Rebase And Merge**, only the commits you push will be kept (no squashing, no merge commit). 25 | If you new to Git, start by reading these [guidelines](https://chris.beams.io/posts/git-commit/). 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RabbitMQ-mock 2 | 3 | [![Build Status](https://github.com/fridujo/rabbitmq-mock/actions/workflows/build.yml/badge.svg)](https://github.com/fridujo/rabbitmq-mock/actions) 4 | [![Coverage Status](https://codecov.io/gh/fridujo/rabbitmq-mock/branch/master/graph/badge.svg)](https://codecov.io/gh/fridujo/rabbitmq-mock/) 5 | [![Maven Central](https://img.shields.io/maven-central/v/com.github.fridujo/rabbitmq-mock.svg)](https://search.maven.org/artifact/com.github.fridujo/rabbitmq-mock) 6 | [![JitPack](https://jitpack.io/v/fridujo/rabbitmq-mock.svg)](https://jitpack.io/#fridujo/rabbitmq-mock) 7 | [![License](https://img.shields.io/github/license/fridujo/rabbitmq-mock.svg)](https://opensource.org/licenses/Apache-2.0) 8 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Ffridujo%2Frabbitmq-mock.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Ffridujo%2Frabbitmq-mock?ref=badge_shield) 9 | 10 | Mock for RabbitMQ Java [amqp-client](https://github.com/rabbitmq/rabbitmq-java-client). 11 | 12 | > Compatible with versions **4.0.0** to **5+** of [**com.rabbitmq:amqp-client**](https://github.com/rabbitmq/rabbitmq-java-client) 13 | 14 | > Compatible with versions **3.6.3** to **4.0.0** with the [`com.github.fridujo.rabbitmq.mock.compatibility` package](src/main/java/com/github/fridujo/rabbitmq/mock/compatibility/MockConnectionFactoryFactory.java). 15 | 16 | ### Motivation 17 | 18 | This project aims to emulate RabbitMQ behavior for test purposes, through `com.rabbitmq.client.ConnectionFactory` with [`MockConnectionFactory`](src/main/java/com/github/fridujo/rabbitmq/mock/MockConnectionFactory.java). 19 | 20 | However today, you will have more robust results using a real RabbitMQ instance through the use of [Testcontainers](https://java.testcontainers.org/modules/rabbitmq/). 21 | 22 | If Docker is not an acceptable option, you can still rely on **RabbitMQ-mock**. 23 | 24 | ## Example Use 25 | 26 | Replace the use of `com.rabbitmq.client.ConnectionFactory` by [`MockConnectionFactory`](src/main/java/com/github/fridujo/rabbitmq/mock/MockConnectionFactory.java) 27 | 28 | ```java 29 | ConnectionFactory factory = new MockConnectionFactory(); 30 | try (Connection conn = factory.newConnection()) { 31 | try (Channel channel = conn.createChannel()) { 32 | GetResponse response = channel.basicGet(queueName, autoAck); 33 | byte[] body = response.getBody(); 34 | long deliveryTag = response.getEnvelope().getDeliveryTag(); 35 | 36 | // Do what you need with the body 37 | 38 | channel.basicAck(deliveryTag, false); 39 | } 40 | } 41 | ``` 42 | 43 | More details in [integration-test](src/test/java/com/github/fridujo/rabbitmq/mock/IntegrationTest.java) 44 | 45 | ### With Spring 46 | Change underlying RabbitMQ ConnectionFactory by [`MockConnectionFactory`](src/main/java/com/github/fridujo/rabbitmq/mock/MockConnectionFactory.java) 47 | 48 | ```java 49 | 50 | @Configuration 51 | @Import(AppConfiguration.class) 52 | class TestConfiguration { 53 | @Bean 54 | ConnectionFactory connectionFactory() { 55 | return new CachingConnectionFactory(new MockConnectionFactory()); 56 | } 57 | } 58 | ``` 59 | 60 | More details in [integration-test](src/test/java/com/github/fridujo/rabbitmq/mock/spring/SpringIntegrationTest.java) 61 | 62 | ## Contribute 63 | Any contribution is greatly appreciated. Please check out the [guide](CONTRIBUTING.md) for more details. 64 | 65 | [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#github.com/fridujo/rabbitmq-mock.git) 66 | 67 | ## Getting Started 68 | 69 | ### Maven 70 | Add the following dependency to your **pom.xml** 71 | ```xml 72 | 73 | com.github.fridujo 74 | rabbitmq-mock 75 | ${rabbitmq-mock.version} 76 | test 77 | 78 | ``` 79 | 80 | ### Gradle 81 | Add the following dependency to your **build.gradle** 82 | ```groovy 83 | repositories { 84 | mavenCentral() 85 | } 86 | 87 | // ... 88 | 89 | dependencies { 90 | // ... 91 | testCompile('com.github.fridujo:rabbitmq-mock:$rabbitmqMockVersion') 92 | // ... 93 | } 94 | ``` 95 | 96 | ### Building from Source 97 | 98 | You need [JDK-17(https://adoptium.net/temurin/releases/?version=17&package=jdk) to build RabbitMQ-Mock. The project can be built with Maven using the following command. 99 | ``` 100 | ./mvnw install 101 | ``` 102 | 103 | Tests are split in: 104 | 105 | * **unit tests** covering features and borderline cases: `mvn test` 106 | * **integration tests**, seatbelts for integration with Spring and Spring-Boot. These tests use the **maven-invoker-plugin** to launch the same project (in [src/it/spring_boot](src/it/spring_boot)) with different versions of the dependencies: `mvn integration-test` 107 | * **mutation tests**, to help understand what is missing in test assertions: `mvn org.pitest:pitest-maven:mutationCoverage` 108 | 109 | ### Using the latest SNAPSHOT 110 | 111 | The master of the project pushes SNAPSHOTs in Sonatype's repo. 112 | 113 | To use the latest master build add Sonatype OSS snapshot repository, for Maven: 114 | ``` 115 | 116 | ... 117 | 118 | sonatype-oss-spanshots 119 | https://oss.sonatype.org/content/repositories/snapshots 120 | 121 | 122 | ``` 123 | 124 | For Gradle: 125 | ```groovy 126 | repositories { 127 | // ... 128 | maven { 129 | url "https://oss.sonatype.org/content/repositories/snapshots" 130 | } 131 | } 132 | ``` 133 | 134 | ## License 135 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Ffridujo%2Frabbitmq-mock.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Ffridujo%2Frabbitmq-mock?ref=badge_large) 136 | -------------------------------------------------------------------------------- /docs/antora.yml: -------------------------------------------------------------------------------- 1 | name: rabbitmq-mock 2 | title: RabbitMQ-Mock 3 | version: 1.3.0-SNAPSHOT 4 | 5 | nav: 6 | - modules/ROOT/nav.adoc 7 | -------------------------------------------------------------------------------- /docs/modules/ROOT/nav.adoc: -------------------------------------------------------------------------------- 1 | * xref:index.adoc[RabbitMQ-Mock] 2 | * Getting started 3 | ** xref:index.adoc#maven[Maven] 4 | ** xref:index.adoc#gradle[Gradle] 5 | * xref:index.adoc#use[Your first test] 6 | * xref:index.adoc#spring[Integrating with Spring] 7 | * xref:index.adoc#plugins[Custom plugins] 8 | -------------------------------------------------------------------------------- /docs/modules/ROOT/pages/index.adoc: -------------------------------------------------------------------------------- 1 | = RabbitMQ-Mock Documentation 2 | :keywords: rabbitmq, test, junit, spring, qpid, broker 3 | 4 | This site hosts the technical documentation for RabbitMQ-Mock. 5 | 6 | [%hardbreaks] 7 | This project aims to emulate RabbitMQ behavior for test purposes. 8 | *RabbitMQ-Mock* makes it easy to test against a RabbitMQ broker without having to start one, or using Qpid as a replacement. 9 | 10 | 11 | == Getting started 12 | 13 | [#maven] 14 | === Maven 15 | 16 | Add the following dependency to the *pom.xml* file 17 | [source,xml] 18 | ---- 19 | 20 | com.github.fridujo 21 | rabbitmq-mock 22 | {page-component-version} 23 | test 24 | 25 | ---- 26 | 27 | [#gradle] 28 | === Gradle 29 | 30 | Add the following dependency to the *build.gradle* 31 | [source,groovy] 32 | ---- 33 | repositories { 34 | mavenCentral() 35 | } 36 | 37 | // ... 38 | 39 | dependencies { 40 | // ... 41 | testImplementation('com.github.fridujo:rabbitmq-mock:{page-component-version}') 42 | // ... 43 | } 44 | ---- 45 | 46 | [#use] 47 | == When using ConnectionFactory directly 48 | 49 | Replace the use of `com.rabbitmq.client.ConnectionFactory` by link:https://github.com/fridujo/rabbitmq-mock/blob/{page-component-version}/src/main/java/com/github/fridujo/rabbitmq/mock/MockConnectionFactory.java[`MockConnectionFactory`] 50 | [source,java] 51 | ---- 52 | ConnectionFactory factory = new MockConnectionFactory(); # <1> 53 | try (Connection conn = factory.newConnection()) { 54 | try (Channel channel = conn.createChannel()) { 55 | GetResponse response = channel.basicGet(queueName, autoAck); 56 | byte[] body = response.getBody(); 57 | long deliveryTag = response.getEnvelope().getDeliveryTag(); 58 | 59 | // Do what you need with the body 60 | 61 | channel.basicAck(deliveryTag, false); 62 | } 63 | } 64 | ---- 65 | <1> The only thing that changes: substitute the `ConnectionFactory` with the mock one. 66 | 67 | [#spring] 68 | == When using Spring 69 | 70 | Change underlying RabbitMQ `ConnectionFactory` by link:https://github.com/fridujo/rabbitmq-mock/blob/{page-component-version}/src/main/java/com/github/fridujo/rabbitmq/mock/MockConnectionFactory.java[`MockConnectionFactory`] 71 | [source,java] 72 | ---- 73 | @Configuration 74 | @Import(AppConfiguration.class) # <2> 75 | class TestConfiguration { # <1> 76 | @Bean ConnectionFactory connectionFactory() { # <3> 77 | return new CachingConnectionFactory(new MockConnectionFactory()); 78 | } 79 | } 80 | ---- 81 | <1> Define or reuse a *Spring* configuration dedicated for integration tests 82 | <2> Import the configuration (maybe partial) containing RabbitMq configuration 83 | <3> Override the `connectionFactory` bean with the *Spring* wrapper delegating to `MockConnectionFactory` 84 | 85 | [#plugins] 86 | == Plugins 87 | [%hardbreaks] 88 | Some plugins, like the link:https://github.com/rabbitmq/rabbitmq-sharding[*rabbitmq-sharding*] one, adds more exchange types (ex: `"x-modulus-hash"`). 89 | *RabbitMQ-Mock* supports default *exchange* types: direct, fanout, topic and headers. 90 | 91 | You can enable the built-in plugin link:https://github.com/rabbitmq/rabbitmq-consistent-hash-exchange[*consistent-hash-exchange*]: 92 | [source,java] 93 | ---- 94 | var connectionFactory = new MockConnectionFactory().enableConsistentHashPlugin(); 95 | ---- 96 | 97 | To add a new type of exchange, use the `withAdditionalExchange` method on a `MockConnectionFactory`. 98 | For example: 99 | [source,java] 100 | ---- 101 | var connectionFactory = new MockConnectionFactory().withAdditionalExchange(new FixDelayExchangeCreator()); 102 | ---- 103 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /src/it/spring_boot/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.github.fridujo 8 | rabbitmq-mock-spring-boot-integration 9 | 0.0.1-SNAPSHOT 10 | 11 | RabbitMQ-Mock Spring Boot Integration 12 | Integration of RabbitMQ-Mock with Spring Boot 13 | 14 | 15 | UTF-8 16 | 1.8 17 | 1.8 18 | 19 | @spring-boot.version@ 20 | @rabbitmq-mock.version@ 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-dependencies 31 | ${spring-boot.version} 32 | pom 33 | import 34 | 35 | 36 | 37 | 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-amqp 42 | 43 | 44 | com.rabbitmq 45 | http-client 46 | 47 | 48 | 49 | 50 | 51 | com.github.fridujo 52 | rabbitmq-mock 53 | ${rabbitmq-mock.version} 54 | test 55 | 56 | 57 | org.springframework.boot 58 | spring-boot-starter-test 59 | test 60 | 61 | 62 | junit 63 | junit 64 | test 65 | 66 | 67 | org.assertj 68 | assertj-core 69 | test 70 | 71 | 72 | 73 | 74 | 75 | Maven Central 76 | https://repo.maven.apache.org/maven2/ 77 | 78 | 79 | Spring Milestone 80 | https://repo.spring.io/milestone/ 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/it/spring_boot/src/main/java/com/github/fridujo/rabbitmq/mock/integration/springboot/AmqpApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.integration.springboot; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | import java.io.IOException; 5 | import java.io.UncheckedIOException; 6 | 7 | import com.rabbitmq.client.Channel; 8 | import org.springframework.amqp.core.Binding; 9 | import org.springframework.amqp.core.BindingBuilder; 10 | import org.springframework.amqp.core.Queue; 11 | import org.springframework.amqp.core.TopicExchange; 12 | import org.springframework.amqp.rabbit.connection.Connection; 13 | import org.springframework.amqp.rabbit.connection.ConnectionFactory; 14 | import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; 15 | import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter; 16 | import org.springframework.boot.SpringApplication; 17 | import org.springframework.boot.autoconfigure.SpringBootApplication; 18 | import org.springframework.context.ConfigurableApplicationContext; 19 | import org.springframework.context.annotation.Bean; 20 | 21 | @SpringBootApplication 22 | public class AmqpApplication { 23 | 24 | public final static String QUEUE_NAME = "spring-boot"; 25 | 26 | public static void main(String[] args) throws InterruptedException { 27 | try (ConfigurableApplicationContext context = SpringApplication.run(AmqpApplication.class, args)) { 28 | rawConfiguration(context.getBean(ConnectionFactory.class)); 29 | 30 | context.getBean(Sender.class).send(); 31 | Receiver receiver = context.getBean(Receiver.class); 32 | while (receiver.getMessages().isEmpty()) { 33 | TimeUnit.MILLISECONDS.sleep(100L); 34 | } 35 | } 36 | } 37 | 38 | private static void rawConfiguration(ConnectionFactory connectionFactory) { 39 | try { 40 | // Connection & Channel may not yet implement AutoCloseable 41 | Connection connection = connectionFactory.createConnection(); 42 | Channel channel = connection.createChannel(false); 43 | channel.exchangeDeclare("xyz", "direct", true); 44 | } catch(IOException e) { 45 | throw new UncheckedIOException("Failed to declare an Exchange using Channel directly", e); 46 | } 47 | } 48 | 49 | @Bean 50 | public Queue queue() { 51 | return new Queue(QUEUE_NAME, false); 52 | } 53 | 54 | @Bean 55 | public TopicExchange exchange() { 56 | return new TopicExchange("spring-boot-exchange"); 57 | } 58 | 59 | @Bean 60 | public Binding binding(Queue queue, TopicExchange exchange) { 61 | return BindingBuilder.bind(queue).to(exchange).with(QUEUE_NAME); 62 | } 63 | 64 | @Bean 65 | public SimpleMessageListenerContainer container(ConnectionFactory connectionFactory, 66 | MessageListenerAdapter listenerAdapter) { 67 | SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); 68 | container.setConnectionFactory(connectionFactory); 69 | container.setQueueNames(QUEUE_NAME); 70 | container.setMessageListener(listenerAdapter); 71 | return container; 72 | } 73 | 74 | @Bean 75 | public MessageListenerAdapter listenerAdapter(Receiver receiver) { 76 | return new MessageListenerAdapter(receiver, "receiveMessage"); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/it/spring_boot/src/main/java/com/github/fridujo/rabbitmq/mock/integration/springboot/Receiver.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.integration.springboot; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | @Component 9 | public class Receiver { 10 | 11 | private final List messages = new ArrayList<>(); 12 | 13 | public void receiveMessage(String message) { 14 | System.out.println("Received <" + message + ">"); 15 | this.messages.add(message); 16 | } 17 | 18 | public List getMessages() { 19 | return messages; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/it/spring_boot/src/main/java/com/github/fridujo/rabbitmq/mock/integration/springboot/Sender.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.integration.springboot; 2 | 3 | import org.springframework.amqp.rabbit.core.RabbitTemplate; 4 | import org.springframework.stereotype.Component; 5 | 6 | @Component 7 | public class Sender { 8 | 9 | private final RabbitTemplate rabbitTemplate; 10 | 11 | public Sender(RabbitTemplate rabbitTemplate) { 12 | this.rabbitTemplate = rabbitTemplate; 13 | } 14 | 15 | public void send() { 16 | System.out.println("Sending message..."); 17 | rabbitTemplate.convertAndSend("", AmqpApplication.QUEUE_NAME, "Hello from RabbitMQ!"); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/it/spring_boot/src/test/java/com/github/fridujo/rabbitmq/mock/integration/springboot/AmqpApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.integration.springboot; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.test.context.ContextConfiguration; 7 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | 15 | @RunWith(SpringJUnit4ClassRunner.class) 16 | @ContextConfiguration(classes = AmqpApplicationTestConfiguration.class) 17 | public class AmqpApplicationTest { 18 | 19 | @Autowired 20 | private Sender sender; 21 | @Autowired 22 | private Receiver receiver; 23 | 24 | @Test 25 | public void message_is_received_when_sent_by_sender() throws InterruptedException { 26 | sender.send(); 27 | List receivedMessages = new ArrayList<>(); 28 | while (receivedMessages.isEmpty()) { 29 | receivedMessages.addAll(receiver.getMessages()); 30 | TimeUnit.MILLISECONDS.sleep(100L); 31 | } 32 | 33 | assertThat(receivedMessages).containsExactly("Hello from RabbitMQ!"); 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/it/spring_boot/src/test/java/com/github/fridujo/rabbitmq/mock/integration/springboot/AmqpApplicationTestConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.integration.springboot; 2 | 3 | import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; 4 | import org.springframework.amqp.rabbit.connection.ConnectionFactory; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.Import; 8 | 9 | import com.github.fridujo.rabbitmq.mock.compatibility.MockConnectionFactoryFactory; 10 | 11 | @Configuration 12 | @Import(AmqpApplication.class) 13 | class AmqpApplicationTestConfiguration { 14 | 15 | @Bean 16 | public ConnectionFactory connectionFactory() { 17 | return new CachingConnectionFactory(MockConnectionFactoryFactory.build().enableConsistentHashPlugin()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/AmqArguments.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock; 2 | 3 | import static java.util.Collections.emptyMap; 4 | 5 | import java.util.Arrays; 6 | import java.util.Map; 7 | import java.util.Optional; 8 | 9 | public class AmqArguments { 10 | public static final String DEAD_LETTER_EXCHANGE_KEY = "x-dead-letter-exchange"; 11 | public static final String DEAD_LETTER_ROUTING_KEY_KEY = "x-dead-letter-routing-key"; 12 | public static final String MESSAGE_TTL_KEY = "x-message-ttl"; 13 | public static final String QUEUE_MAX_LENGTH_KEY = "x-max-length"; 14 | public static final String QUEUE_MAX_LENGTH_BYTES_KEY = "x-max-length-bytes"; 15 | public static final String OVERFLOW_KEY = "x-overflow"; 16 | public static final String MAX_PRIORITY_KEY = "x-max-priority"; 17 | private final String ALTERNATE_EXCHANGE_KEY = "alternate-exchange"; 18 | private final Map arguments; 19 | 20 | public static AmqArguments empty() { 21 | return new AmqArguments(emptyMap()); 22 | } 23 | 24 | public AmqArguments(Map arguments) { 25 | this.arguments = arguments; 26 | } 27 | 28 | public Optional getAlternateExchange() { 29 | return string(ALTERNATE_EXCHANGE_KEY) 30 | .map(aeName -> new ReceiverPointer(ReceiverPointer.Type.EXCHANGE, aeName)); 31 | } 32 | 33 | public Optional getDeadLetterExchange() { 34 | return string(DEAD_LETTER_EXCHANGE_KEY) 35 | .map(aeName -> new ReceiverPointer(ReceiverPointer.Type.EXCHANGE, aeName)); 36 | } 37 | 38 | public Optional getDeadLetterRoutingKey() { 39 | return string(DEAD_LETTER_ROUTING_KEY_KEY); 40 | } 41 | 42 | public Optional queueLengthLimit() { 43 | return positiveInteger(QUEUE_MAX_LENGTH_KEY); 44 | } 45 | 46 | public Optional queueLengthBytesLimit() { 47 | return positiveInteger(QUEUE_MAX_LENGTH_BYTES_KEY); 48 | } 49 | 50 | public Overflow overflow() { 51 | return string(OVERFLOW_KEY) 52 | .flatMap(Overflow::parse) 53 | .orElse(Overflow.DROP_HEAD); 54 | } 55 | 56 | public Optional getMessageTtlOfQueue() { 57 | return Optional.ofNullable(arguments.get(MESSAGE_TTL_KEY)) 58 | .filter(aeObject -> aeObject instanceof Number) 59 | .map(Number.class::cast) 60 | .map(number -> number.longValue()); 61 | } 62 | 63 | public Optional queueMaxPriority() { 64 | return positiveInteger(MAX_PRIORITY_KEY) 65 | .filter(i -> i < 256) 66 | .map(Integer::shortValue); 67 | } 68 | 69 | private Optional positiveInteger(String key) { 70 | return Optional.ofNullable(arguments.get(key)) 71 | .filter(aeObject -> aeObject instanceof Number) 72 | .map(Number.class::cast) 73 | .map(num -> num.intValue()) 74 | .filter(i -> i > 0); 75 | } 76 | 77 | private Optional string(String key) { 78 | return Optional.ofNullable(arguments.get(key)) 79 | .filter(aeObject -> aeObject instanceof String) 80 | .map(String.class::cast); 81 | } 82 | 83 | public enum Overflow { 84 | DROP_HEAD("drop-head"), REJECT_PUBLISH("reject-publish"); 85 | 86 | private final String stringValue; 87 | 88 | Overflow(String stringValue) { 89 | this.stringValue = stringValue; 90 | } 91 | 92 | private static Optional parse(String value) { 93 | return Arrays.stream(values()).filter(v -> value.equals(v.stringValue)).findFirst(); 94 | } 95 | 96 | @Override 97 | public String toString() { 98 | return stringValue; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/AmqpExceptions.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock; 2 | 3 | import java.io.IOException; 4 | 5 | import com.rabbitmq.client.Channel; 6 | import com.rabbitmq.client.ShutdownSignalException; 7 | import com.rabbitmq.client.impl.AMQImpl; 8 | 9 | public abstract class AmqpExceptions { 10 | 11 | public static IOException inequivalentExchangeRedeclare(Channel ref, 12 | String vhost, 13 | String exchangeName, 14 | String currentType, 15 | String receivedType) { 16 | StringBuilder replyText = new StringBuilder("PRECONDITION_FAILED - inequivalent arg 'type' ") 17 | .append("for exchange '").append(exchangeName).append("' ") 18 | .append("in vhost '").append(vhost).append("' ") 19 | .append("received '").append(receivedType).append("' ") 20 | .append("but current is '").append(currentType).append("'"); 21 | 22 | AMQImpl.Channel.Close reason = new AMQImpl.Channel.Close( 23 | 406, 24 | replyText.toString(), 25 | AMQImpl.Exchange.INDEX, 26 | AMQImpl.Exchange.Declare.INDEX); 27 | ShutdownSignalException sse = new ShutdownSignalException( 28 | false, 29 | false, 30 | reason, 31 | ref); 32 | return new IOException(sse.sensibleClone()); 33 | } 34 | 35 | public static IOException exchangeNotFound(Channel ref, 36 | String vhost, 37 | String exchangeName) { 38 | StringBuilder replyText = new StringBuilder("NOT_FOUND - no exchange '").append(exchangeName).append("' ") 39 | .append("in vhost '").append(vhost).append("'"); 40 | 41 | ShutdownSignalException reason = new ShutdownSignalException( 42 | false, 43 | false, 44 | new AMQImpl.Channel.Close( 45 | 404, 46 | replyText.toString(), 47 | AMQImpl.Exchange.INDEX, 48 | AMQImpl.Exchange.Declare.INDEX), 49 | ref); 50 | return new IOException(reason.sensibleClone()); 51 | } 52 | 53 | public static IOException queueNotFound(Channel ref, 54 | String vhost, 55 | String queueName) { 56 | StringBuilder replyText = new StringBuilder("NOT_FOUND - no queue '").append(queueName).append("' ") 57 | .append("in vhost '").append(vhost).append("'"); 58 | 59 | ShutdownSignalException reason = new ShutdownSignalException( 60 | false, 61 | false, 62 | new AMQImpl.Channel.Close( 63 | 404, 64 | replyText.toString(), 65 | AMQImpl.Queue.INDEX, 66 | AMQImpl.Queue.Declare.INDEX), 67 | ref); 68 | return new IOException(reason.sensibleClone()); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/ConfigurableConnectionFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock; 2 | 3 | import static com.github.fridujo.rabbitmq.mock.exchange.MockExchangeCreator.creatorWithExchangeType; 4 | 5 | import com.github.fridujo.rabbitmq.mock.exchange.ConsistentHashExchange; 6 | import com.github.fridujo.rabbitmq.mock.exchange.TypedMockExchangeCreator; 7 | import com.rabbitmq.client.ConnectionFactory; 8 | 9 | public abstract class ConfigurableConnectionFactory extends ConnectionFactory { 10 | 11 | protected final MockNode mockNode = new MockNode(); 12 | 13 | @SuppressWarnings("unchecked") 14 | public T withAdditionalExchange(TypedMockExchangeCreator mockExchangeCreator) { 15 | mockNode.getConfiguration().registerAdditionalExchangeCreator(mockExchangeCreator); 16 | return (T) this; 17 | } 18 | 19 | /** 20 | * Make available the {@value ConsistentHashExchange#TYPE}'' exchange. 21 | *

22 | * See https://github.com/rabbitmq/rabbitmq-consistent-hash-exchange. 23 | * @return this {@link ConfigurableConnectionFactory} instance (for chaining) 24 | */ 25 | public T enableConsistentHashPlugin() { 26 | return withAdditionalExchange( 27 | creatorWithExchangeType(ConsistentHashExchange.TYPE, ConsistentHashExchange::new) 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/ConfirmListenerWrapper.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock; 2 | 3 | import com.rabbitmq.client.ConfirmCallback; 4 | import com.rabbitmq.client.ConfirmListener; 5 | 6 | import java.io.IOException; 7 | 8 | public class ConfirmListenerWrapper implements ConfirmListener { 9 | private final ConfirmCallback ackCallback; 10 | private final ConfirmCallback nackCallback; 11 | 12 | public ConfirmListenerWrapper(ConfirmCallback ackCallback, ConfirmCallback nackCallback) { 13 | this.ackCallback = ackCallback; 14 | this.nackCallback = nackCallback; 15 | } 16 | 17 | @Override 18 | public void handleAck(long deliveryTag, boolean multiple) throws IOException { 19 | ackCallback.handle(deliveryTag, multiple); 20 | } 21 | 22 | @Override 23 | public void handleNack(long deliveryTag, boolean multiple) throws IOException { 24 | nackCallback.handle(deliveryTag, multiple); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/ConsumerWrapper.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock; 2 | 3 | import com.rabbitmq.client.AMQP; 4 | import com.rabbitmq.client.CancelCallback; 5 | import com.rabbitmq.client.Consumer; 6 | import com.rabbitmq.client.ConsumerShutdownSignalCallback; 7 | import com.rabbitmq.client.DeliverCallback; 8 | import com.rabbitmq.client.Delivery; 9 | import com.rabbitmq.client.Envelope; 10 | import com.rabbitmq.client.ShutdownSignalException; 11 | 12 | import java.io.IOException; 13 | 14 | public class ConsumerWrapper implements Consumer { 15 | private final DeliverCallback deliverCallback; 16 | private final CancelCallback cancelCallback; 17 | private final ConsumerShutdownSignalCallback shutdownSignalCallback; 18 | 19 | public ConsumerWrapper(DeliverCallback deliverCallback, CancelCallback cancelCallback, ConsumerShutdownSignalCallback shutdownSignalCallback) { 20 | this.deliverCallback = deliverCallback; 21 | this.cancelCallback = cancelCallback; 22 | this.shutdownSignalCallback = shutdownSignalCallback; 23 | } 24 | 25 | @Override 26 | public void handleConsumeOk(String consumerTag) { 27 | // Nothing to be done 28 | } 29 | 30 | @Override 31 | public void handleCancelOk(String consumerTag) { 32 | // Nothing to be done 33 | } 34 | 35 | @Override 36 | public void handleCancel(String consumerTag) throws IOException { 37 | if (cancelCallback != null) { 38 | cancelCallback.handle(consumerTag); 39 | } 40 | } 41 | 42 | @Override 43 | public void handleShutdownSignal(String consumerTag, ShutdownSignalException sig) { 44 | if (shutdownSignalCallback != null) { 45 | shutdownSignalCallback.handleShutdownSignal(consumerTag, sig); 46 | } 47 | } 48 | 49 | @Override 50 | public void handleRecoverOk(String consumerTag) { 51 | // Nothing to be done 52 | } 53 | 54 | @Override 55 | public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { 56 | deliverCallback.handle(consumerTag, new Delivery(envelope, properties, body)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/DeadLettering.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.Optional; 9 | 10 | import com.rabbitmq.client.AMQP; 11 | 12 | public interface DeadLettering { 13 | 14 | String X_DEATH_HEADER = "x-death"; 15 | 16 | enum ReasonType { 17 | /** 18 | * The message was rejected with requeue parameter set to false. 19 | */ 20 | REJECTED("rejected"), 21 | /** 22 | * The message TTL has expired. 23 | */ 24 | EXPIRED("expired"), 25 | /** 26 | * The maximum allowed queue length was exceeded. 27 | */ 28 | MAX_LEN("maxlen"); 29 | 30 | public final String headerValue; 31 | 32 | ReasonType(String headerValue) { 33 | this.headerValue = headerValue; 34 | } 35 | } 36 | 37 | class Event { 38 | private static final String QUEUE_KEY = "queue"; 39 | private static final String REASON_KEY = "reason"; 40 | private static final String EXCHANGE_KEY = "exchange"; 41 | private static final String ROUTING_KEYS_KEY = "routing-keys"; 42 | private static final String COUNT_KEY = "count"; 43 | 44 | private final String queue; 45 | private final ReasonType reason; 46 | private final String exchange; 47 | private final List routingKeys; 48 | private final long count; 49 | 50 | public Event(String queue, ReasonType reason, Message message, int count) { 51 | this.queue = queue; 52 | this.reason = reason; 53 | this.exchange = message.exchangeName; 54 | this.routingKeys = Collections.singletonList(message.routingKey); 55 | this.count = count; 56 | } 57 | 58 | public Map asHeaderEntry() { 59 | Map entry = new HashMap<>(); 60 | entry.put(QUEUE_KEY, queue); 61 | entry.put(REASON_KEY, reason.headerValue); 62 | entry.put(EXCHANGE_KEY, exchange); 63 | entry.put(ROUTING_KEYS_KEY, routingKeys); 64 | entry.put(COUNT_KEY, count); 65 | return entry; 66 | } 67 | 68 | @SuppressWarnings("unchecked") 69 | public AMQP.BasicProperties prependOn(AMQP.BasicProperties props) { 70 | Map headers = Optional.ofNullable(props.getHeaders()).map(HashMap::new).orElseGet(HashMap::new); 71 | 72 | List> xDeathHeader = (List>) headers.computeIfAbsent(X_DEATH_HEADER, key -> new ArrayList<>()); 73 | 74 | Optional> previousEvent = xDeathHeader.stream() 75 | .filter(this::sameQueueAndReason) 76 | .findFirst(); 77 | 78 | final Map currentEvent; 79 | if (previousEvent.isPresent()) { 80 | xDeathHeader.remove(previousEvent.get()); 81 | currentEvent = incrementCount(previousEvent.get()); 82 | } else { 83 | currentEvent = asHeaderEntry(); 84 | } 85 | xDeathHeader.add(0, currentEvent); 86 | 87 | return props.builder().headers(Collections.unmodifiableMap(headers)).build(); 88 | } 89 | 90 | private Map incrementCount(Map previousEvent) { 91 | previousEvent.compute(COUNT_KEY, (key, count) -> (long) count + 1); 92 | return previousEvent; 93 | } 94 | 95 | private boolean sameQueueAndReason(Map event) { 96 | return queue.equals(event.get(QUEUE_KEY)) && reason.headerValue.equals(event.get(REASON_KEY)); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/Message.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock; 2 | 3 | import com.rabbitmq.client.AMQP; 4 | 5 | import java.time.Instant; 6 | import java.util.Optional; 7 | 8 | public class Message { 9 | 10 | public final int id; 11 | public final String exchangeName; 12 | public final String routingKey; 13 | public final AMQP.BasicProperties props; 14 | public final byte[] body; 15 | public final long expiryTime; 16 | public final boolean redelivered; 17 | 18 | public Message(int id, String exchangeName, String routingKey, AMQP.BasicProperties props, byte[] body, long expiryTime) { 19 | this(id, exchangeName, routingKey, props, body, expiryTime, false); 20 | } 21 | 22 | private Message(int id, String exchangeName, String routingKey, AMQP.BasicProperties props, byte[] body, long expiryTime, boolean redelivered) { 23 | this.id = id; 24 | this.exchangeName = exchangeName; 25 | this.routingKey = routingKey; 26 | this.props = props; 27 | this.body = body; 28 | this.expiryTime = expiryTime; 29 | this.redelivered = redelivered; 30 | } 31 | 32 | public Message asRedelivered() { 33 | return new Message(id, exchangeName, routingKey, props, body, expiryTime, true); 34 | } 35 | 36 | public boolean isExpired() { 37 | return expiryTime > -1 && System.currentTimeMillis() > expiryTime; 38 | } 39 | 40 | public int priority() { 41 | return Optional.ofNullable(props.getPriority()).orElse(0); 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | return "Message{" + 47 | "exchangeName='" + exchangeName + '\'' + 48 | ", routingKey='" + routingKey + '\'' + 49 | ", body=" + new String(body) + 50 | ", expiryTime=" + Instant.ofEpochMilli(expiryTime) + 51 | '}'; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/MessageComparator.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock; 2 | 3 | import java.util.Comparator; 4 | 5 | import static java.lang.Math.min; 6 | 7 | public class MessageComparator implements Comparator { 8 | 9 | private final AmqArguments queueArguments; 10 | 11 | public MessageComparator(AmqArguments queueArguments) { 12 | this.queueArguments = queueArguments; 13 | } 14 | 15 | @Override 16 | public int compare(Message m1, Message m2) { 17 | int priorityComparison = queueArguments.queueMaxPriority() 18 | .map(max -> min(max, m2.priority()) - min(max, m1.priority())) 19 | .orElse(0); 20 | int publicationOrderComparison = m1.id - m2.id; 21 | 22 | return priorityComparison != 0 ? priorityComparison : publicationOrderComparison; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/MockConnection.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock; 2 | 3 | import java.net.InetAddress; 4 | import java.net.InetSocketAddress; 5 | import java.util.Collections; 6 | import java.util.Map; 7 | import java.util.concurrent.atomic.AtomicBoolean; 8 | import java.util.concurrent.atomic.AtomicInteger; 9 | 10 | import com.github.fridujo.rabbitmq.mock.metrics.MetricsCollectorWrapper; 11 | import com.rabbitmq.client.AMQP; 12 | import com.rabbitmq.client.AlreadyClosedException; 13 | import com.rabbitmq.client.BlockedCallback; 14 | import com.rabbitmq.client.BlockedListener; 15 | import com.rabbitmq.client.Connection; 16 | import com.rabbitmq.client.ExceptionHandler; 17 | import com.rabbitmq.client.ShutdownListener; 18 | import com.rabbitmq.client.ShutdownSignalException; 19 | import com.rabbitmq.client.UnblockedCallback; 20 | import com.rabbitmq.client.impl.AMQConnection; 21 | import com.rabbitmq.client.impl.DefaultExceptionHandler; 22 | import com.rabbitmq.client.impl.LongStringHelper; 23 | import com.rabbitmq.client.impl.Version; 24 | 25 | public class MockConnection implements Connection { 26 | 27 | private final AtomicBoolean opened = new AtomicBoolean(true); 28 | private final AtomicInteger channelSequence = new AtomicInteger(); 29 | private final MockNode mockNode; 30 | private final MetricsCollectorWrapper metricsCollectorWrapper; 31 | private final InetAddress address; 32 | private final DefaultExceptionHandler exceptionHandler = new DefaultExceptionHandler(); 33 | private String id; 34 | 35 | public MockConnection(MockNode mockNode, MetricsCollectorWrapper metricsCollectorWrapper) { 36 | this.mockNode = mockNode.restartDeliveryLoops(); 37 | this.metricsCollectorWrapper = metricsCollectorWrapper; 38 | this.address = new InetSocketAddress("127.0.0.1", 0).getAddress(); 39 | this.metricsCollectorWrapper.newConnection(this); 40 | } 41 | 42 | @Override 43 | public InetAddress getAddress() { 44 | return address; 45 | } 46 | 47 | @Override 48 | public int getPort() { 49 | return com.rabbitmq.client.ConnectionFactory.DEFAULT_AMQP_PORT; 50 | } 51 | 52 | @Override 53 | public int getChannelMax() { 54 | return 0; 55 | } 56 | 57 | @Override 58 | public int getFrameMax() { 59 | return 0; 60 | } 61 | 62 | @Override 63 | public int getHeartbeat() { 64 | return 0; 65 | } 66 | 67 | @Override 68 | public Map getClientProperties() { 69 | return AMQConnection.defaultClientProperties(); 70 | } 71 | 72 | @Override 73 | public String getClientProvidedName() { 74 | return null; 75 | } 76 | 77 | @Override 78 | public Map getServerProperties() { 79 | return Collections.singletonMap("version", 80 | LongStringHelper.asLongString(new Version(AMQP.PROTOCOL.MAJOR, AMQP.PROTOCOL.MINOR).toString())); 81 | } 82 | 83 | @Override 84 | public MockChannel createChannel() throws AlreadyClosedException { 85 | return createChannel(channelSequence.incrementAndGet()); 86 | } 87 | 88 | @Override 89 | public MockChannel createChannel(int channelNumber) throws AlreadyClosedException { 90 | if (!isOpen()) { 91 | throw new AlreadyClosedException(new ShutdownSignalException(false, true, null, this)); 92 | } 93 | return new MockChannel(channelNumber, mockNode, this, metricsCollectorWrapper); 94 | } 95 | 96 | @Override 97 | public void close() { 98 | close(AMQP.REPLY_SUCCESS, "OK"); 99 | } 100 | 101 | @Override 102 | public void close(int closeCode, String closeMessage) { 103 | close(closeCode, closeMessage, -1); 104 | } 105 | 106 | @Override 107 | public void close(int timeout) { 108 | close(AMQP.REPLY_SUCCESS, "OK", timeout); 109 | } 110 | 111 | @Override 112 | public void close(int closeCode, String closeMessage, int timeout) { 113 | metricsCollectorWrapper.closeConnection(this); 114 | opened.set(false); 115 | mockNode.close(this); 116 | } 117 | 118 | @Override 119 | public void abort() { 120 | abort(AMQP.REPLY_SUCCESS, "OK"); 121 | } 122 | 123 | @Override 124 | public void abort(int closeCode, String closeMessage) { 125 | abort(closeCode, closeMessage, -1); 126 | } 127 | 128 | @Override 129 | public void abort(int timeout) { 130 | abort(AMQP.REPLY_SUCCESS, "OK", timeout); 131 | } 132 | 133 | @Override 134 | public void abort(int closeCode, String closeMessage, int timeout) { 135 | close(closeCode, closeMessage, timeout); 136 | } 137 | 138 | @Override 139 | public void addBlockedListener(BlockedListener listener) { 140 | // do nothing 141 | } 142 | 143 | @Override 144 | public BlockedListener addBlockedListener(BlockedCallback blockedCallback, UnblockedCallback unblockedCallback) { 145 | // do nothing 146 | return null; 147 | } 148 | 149 | @Override 150 | public boolean removeBlockedListener(BlockedListener listener) { 151 | // do nothing 152 | return true; 153 | } 154 | 155 | @Override 156 | public void clearBlockedListeners() { 157 | // do nothing 158 | } 159 | 160 | @Override 161 | public ExceptionHandler getExceptionHandler() { 162 | return exceptionHandler; 163 | } 164 | 165 | @Override 166 | public String getId() { 167 | return id; 168 | } 169 | 170 | @Override 171 | public void setId(String id) { 172 | this.id = id; 173 | } 174 | 175 | @Override 176 | public void addShutdownListener(ShutdownListener listener) { 177 | // do nothing 178 | } 179 | 180 | @Override 181 | public void removeShutdownListener(ShutdownListener listener) { 182 | // do nothing 183 | } 184 | 185 | @Override 186 | public ShutdownSignalException getCloseReason() { 187 | return null; 188 | } 189 | 190 | @Override 191 | public void notifyListeners() { 192 | throw new UnsupportedOperationException(); 193 | } 194 | 195 | @Override 196 | public boolean isOpen() { 197 | return opened.get(); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/MockConnectionFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock; 2 | 3 | import com.github.fridujo.rabbitmq.mock.metrics.MetricsCollectorWrapper; 4 | import com.rabbitmq.client.AddressResolver; 5 | import com.rabbitmq.client.ConnectionFactory; 6 | 7 | import java.util.concurrent.ExecutorService; 8 | 9 | public class MockConnectionFactory extends ConfigurableConnectionFactory { 10 | 11 | public MockConnectionFactory() { 12 | setAutomaticRecoveryEnabled(false); 13 | } 14 | 15 | @Override 16 | public MockConnection newConnection(ExecutorService executor, AddressResolver addressResolver, String clientProvidedName) { 17 | return newConnection(); 18 | } 19 | 20 | public MockConnection newConnection() { 21 | MetricsCollectorWrapper metricsCollectorWrapper = MetricsCollectorWrapper.Builder.build(this); 22 | MockConnection mockConnection = new MockConnection(mockNode, metricsCollectorWrapper); 23 | return mockConnection; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/MockNode.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock; 2 | 3 | import java.util.Map; 4 | import java.util.Optional; 5 | import java.util.concurrent.ConcurrentHashMap; 6 | import java.util.function.Supplier; 7 | 8 | import com.rabbitmq.client.AMQP; 9 | import com.rabbitmq.client.Consumer; 10 | import com.rabbitmq.client.GetResponse; 11 | import com.rabbitmq.client.impl.AMQImpl; 12 | 13 | import com.github.fridujo.rabbitmq.mock.configuration.Configuration; 14 | import com.github.fridujo.rabbitmq.mock.exchange.MockDefaultExchange; 15 | import com.github.fridujo.rabbitmq.mock.exchange.MockExchange; 16 | import com.github.fridujo.rabbitmq.mock.exchange.MockExchangeFactory; 17 | 18 | public class MockNode implements ReceiverRegistry, TransactionalOperations { 19 | 20 | private final Configuration configuration = new Configuration(); 21 | private final MockExchangeFactory mockExchangeFactory = new MockExchangeFactory(configuration); 22 | private final MockDefaultExchange defaultExchange = new MockDefaultExchange(this); 23 | private final Map exchanges = new ConcurrentHashMap<>(); 24 | private final Map queues = new ConcurrentHashMap<>(); 25 | private final RandomStringGenerator consumerTagGenerator = new RandomStringGenerator( 26 | "amq.ctag-", 27 | "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 28 | 22); 29 | 30 | 31 | public MockNode() { 32 | exchanges.put(MockDefaultExchange.NAME, defaultExchange); 33 | } 34 | 35 | public boolean basicPublish(String exchangeName, String routingKey, boolean mandatory, boolean immediate, AMQP.BasicProperties props, byte[] body) { 36 | MockExchange exchange = getExchangeUnchecked(exchangeName); 37 | return exchange.publish(null, routingKey, props, body); 38 | } 39 | 40 | public String basicConsume(String queueName, boolean autoAck, String consumerTag, boolean noLocal, boolean exclusive, Map arguments, Consumer callback, Supplier deliveryTagSupplier, MockConnection mockConnection, MockChannel mockChannel) { 41 | final String definitiveConsumerTag; 42 | if ("".equals(consumerTag)) { 43 | definitiveConsumerTag = consumerTagGenerator.generate(); 44 | } else { 45 | definitiveConsumerTag = consumerTag; 46 | } 47 | 48 | getQueueUnchecked(queueName).basicConsume(definitiveConsumerTag, callback, autoAck, deliveryTagSupplier, mockConnection, mockChannel); 49 | 50 | return definitiveConsumerTag; 51 | } 52 | 53 | public Optional getQueue(String name) { 54 | return Optional.ofNullable(queues.get(name)); 55 | } 56 | 57 | public AMQP.Exchange.DeclareOk exchangeDeclare(String exchangeName, String type, boolean durable, boolean autoDelete, boolean internal, Map arguments) { 58 | exchanges.putIfAbsent(exchangeName, mockExchangeFactory.build(exchangeName, type, new AmqArguments(arguments), this)); 59 | return new AMQImpl.Exchange.DeclareOk(); 60 | } 61 | 62 | public AMQP.Exchange.DeleteOk exchangeDelete(String exchange) { 63 | exchanges.remove(exchange); 64 | return new AMQImpl.Exchange.DeleteOk(); 65 | } 66 | 67 | public AMQP.Exchange.BindOk exchangeBind(String destinationName, String sourceName, String routingKey, Map arguments) { 68 | MockExchange source = getExchangeUnchecked(sourceName); 69 | MockExchange destination = getExchangeUnchecked(destinationName); 70 | source.bind(destination.pointer(), routingKey, arguments); 71 | return new AMQImpl.Exchange.BindOk(); 72 | } 73 | 74 | public AMQP.Exchange.UnbindOk exchangeUnbind(String destinationName, String sourceName, String routingKey, Map arguments) { 75 | MockExchange source = getExchangeUnchecked(sourceName); 76 | MockExchange destination = getExchangeUnchecked(destinationName); 77 | source.unbind(destination.pointer(), routingKey, arguments); 78 | return new AMQImpl.Exchange.UnbindOk(); 79 | } 80 | 81 | public AMQP.Queue.DeclareOk queueDeclare(String queueName, boolean durable, boolean exclusive, boolean autoDelete, Map arguments) { 82 | queues.putIfAbsent(queueName, new MockQueue(queueName, new AmqArguments(arguments), this)); 83 | return new AMQP.Queue.DeclareOk.Builder() 84 | .queue(queueName) 85 | .build(); 86 | } 87 | 88 | public AMQP.Queue.DeleteOk queueDelete(String queueName, boolean ifUnused, boolean ifEmpty) { 89 | Optional queue = Optional.ofNullable(queues.remove(queueName)); 90 | queue.ifPresent(MockQueue::notifyDeleted); 91 | return new AMQImpl.Queue.DeleteOk(queue.map(MockQueue::messageCount).orElse(0)); 92 | } 93 | 94 | public AMQP.Queue.BindOk queueBind(String queueName, String exchangeName, String routingKey, Map arguments) { 95 | MockExchange exchange = getExchangeUnchecked(exchangeName); 96 | MockQueue queue = getQueueUnchecked(queueName); 97 | exchange.bind(queue.pointer(), routingKey, arguments); 98 | return new AMQImpl.Queue.BindOk(); 99 | } 100 | 101 | public AMQP.Queue.UnbindOk queueUnbind(String queueName, String exchangeName, String routingKey, Map arguments) { 102 | MockExchange exchange = getExchangeUnchecked(exchangeName); 103 | MockQueue queue = getQueueUnchecked(queueName); 104 | exchange.unbind(queue.pointer(), routingKey, arguments); 105 | return new AMQImpl.Queue.UnbindOk(); 106 | } 107 | 108 | public AMQP.Queue.PurgeOk queuePurge(String queueName) { 109 | MockQueue queue = getQueueUnchecked(queueName); 110 | int messageCount = queue.purge(); 111 | return new AMQImpl.Queue.PurgeOk(messageCount); 112 | } 113 | 114 | public GetResponse basicGet(String queueName, boolean autoAck, Supplier deliveryTagSupplier) { 115 | MockQueue queue = getQueueUnchecked(queueName); 116 | return queue.basicGet(autoAck, deliveryTagSupplier); 117 | } 118 | 119 | public void basicAck(long deliveryTag, boolean multiple) { 120 | queues.values().forEach(q -> q.basicAck(deliveryTag, multiple)); 121 | } 122 | 123 | public void basicNack(long deliveryTag, boolean multiple, boolean requeue) { 124 | queues.values().forEach(q -> q.basicNack(deliveryTag, multiple, requeue)); 125 | } 126 | 127 | public void basicReject(long deliveryTag, boolean requeue) { 128 | queues.values().forEach(q -> q.basicReject(deliveryTag, requeue)); 129 | } 130 | 131 | public void basicCancel(String consumerTag) { 132 | queues.values().forEach(q -> q.basicCancel(consumerTag)); 133 | } 134 | 135 | public AMQP.Basic.RecoverOk basicRecover(boolean requeue) { 136 | queues.values().forEach(q -> q.basicRecover(requeue)); 137 | return new AMQImpl.Basic.RecoverOk(); 138 | } 139 | 140 | @Override 141 | public Optional getReceiver(ReceiverPointer receiverPointer) { 142 | final Optional receiver; 143 | if (receiverPointer.type == ReceiverPointer.Type.EXCHANGE) { 144 | receiver = Optional.ofNullable(exchanges.get(receiverPointer.name)); 145 | } else { 146 | receiver = Optional.ofNullable(queues.get(receiverPointer.name)); 147 | } 148 | return receiver; 149 | } 150 | 151 | 152 | private MockExchange getExchangeUnchecked(String exchangeName) { 153 | if (!exchanges.containsKey(exchangeName)) { 154 | throw new IllegalArgumentException("No exchange named " + exchangeName); 155 | } 156 | return exchanges.get(exchangeName); 157 | } 158 | 159 | private MockQueue getQueueUnchecked(String queueName) { 160 | if (!queues.containsKey(queueName)) { 161 | throw new IllegalArgumentException("No queue named " + queueName); 162 | } 163 | return queues.get(queueName); 164 | } 165 | 166 | Optional getExchange(String name) { 167 | return Optional.ofNullable(exchanges.get(name)); 168 | } 169 | 170 | public int messageCount(String queueName) { 171 | MockQueue queue = getQueueUnchecked(queueName); 172 | return queue.messageCount(); 173 | } 174 | 175 | public long consumerCount(String queueName) { 176 | MockQueue queue = getQueueUnchecked(queueName); 177 | return queue.consumerCount(); 178 | } 179 | 180 | public MockNode restartDeliveryLoops() { 181 | queues.values().forEach(MockQueue::restartDeliveryLoop); 182 | return this; 183 | } 184 | 185 | public void close(MockConnection mockConnection) { 186 | queues.values().forEach(q -> q.close(mockConnection)); 187 | } 188 | 189 | public Configuration getConfiguration() { 190 | return configuration; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/RandomStringGenerator.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock; 2 | 3 | import java.util.Random; 4 | import java.util.stream.Collectors; 5 | 6 | class RandomStringGenerator { 7 | 8 | private final String prefix; 9 | private final char[] availableCharacters; 10 | private final int length; 11 | 12 | RandomStringGenerator(String prefix, String availableCharacters, int length) { 13 | this.prefix = prefix; 14 | this.availableCharacters = availableCharacters.toCharArray(); 15 | this.length = length; 16 | } 17 | 18 | String generate() { 19 | return prefix + new Random().ints(length, 0, availableCharacters.length) 20 | .mapToObj(i -> availableCharacters[i]) 21 | .map(String::valueOf) 22 | .collect(Collectors.joining()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/Receiver.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock; 2 | 3 | import com.rabbitmq.client.AMQP; 4 | 5 | /** 6 | * Leverage the receiving capability of both Queues and Exchanges. 7 | */ 8 | public interface Receiver { 9 | String X_MATCH_KEY = "x-match"; 10 | 11 | /** 12 | * @return true if message got routed, false otherwise 13 | */ 14 | boolean publish(String exchangeName, String routingKey, AMQP.BasicProperties props, byte[] body); 15 | 16 | ReceiverPointer pointer(); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/ReceiverPointer.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock; 2 | 3 | import java.util.Objects; 4 | 5 | public class ReceiverPointer { 6 | final Type type; 7 | final String name; 8 | 9 | public ReceiverPointer(Type type, String name) { 10 | this.type = type; 11 | this.name = name; 12 | } 13 | 14 | @Override 15 | public boolean equals(Object o) { 16 | if (this == o) return true; 17 | if (o == null || getClass() != o.getClass()) return false; 18 | ReceiverPointer that = (ReceiverPointer) o; 19 | return type == that.type && 20 | Objects.equals(name, that.name); 21 | } 22 | 23 | @Override 24 | public int hashCode() { 25 | return Objects.hash(type, name); 26 | } 27 | 28 | public enum Type { 29 | QUEUE, EXCHANGE 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/ReceiverRegistry.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock; 2 | 3 | import java.util.Optional; 4 | 5 | public interface ReceiverRegistry { 6 | 7 | Optional getReceiver(ReceiverPointer receiverPointer); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/Transaction.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock; 2 | 3 | import com.rabbitmq.client.AMQP; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | public class Transaction implements TransactionalOperations { 9 | private final MockNode mockNode; 10 | private final List publishedMessages = new ArrayList<>(); 11 | private final List rejects = new ArrayList<>(); 12 | private final List nacks = new ArrayList<>(); 13 | private final List acks = new ArrayList<>(); 14 | 15 | public Transaction(MockNode mockNode) { 16 | this.mockNode = mockNode; 17 | } 18 | 19 | public void commit() { 20 | publishedMessages.forEach(publishedMessage -> mockNode.basicPublish( 21 | publishedMessage.exchange, 22 | publishedMessage.routingKey, 23 | publishedMessage.mandatory, 24 | publishedMessage.immediate, 25 | publishedMessage.props, 26 | publishedMessage.body 27 | )); 28 | publishedMessages.clear(); 29 | 30 | rejects.forEach(reject -> mockNode.basicReject(reject.deliveryTag, reject.requeue)); 31 | rejects.clear(); 32 | 33 | nacks.forEach(nack -> mockNode.basicNack(nack.deliveryTag, nack.multiple, nack.requeue)); 34 | nacks.clear(); 35 | 36 | acks.forEach(ack -> mockNode.basicAck(ack.deliveryTag, ack.multiple)); 37 | acks.clear(); 38 | } 39 | 40 | @Override 41 | public boolean basicPublish(String exchange, String routingKey, boolean mandatory, boolean immediate, AMQP.BasicProperties props, byte[] body) { 42 | publishedMessages.add(new PublishedMessage(exchange, routingKey, mandatory, immediate, props, body)); 43 | return true;//behavior is not defined in spec 44 | } 45 | 46 | @Override 47 | public void basicReject(long deliveryTag, boolean requeue) { 48 | rejects.add(new Reject(deliveryTag, requeue)); 49 | } 50 | 51 | @Override 52 | public void basicNack(long deliveryTag, boolean multiple, boolean requeue) { 53 | nacks.add(new Nack(deliveryTag, multiple, requeue)); 54 | } 55 | 56 | @Override 57 | public void basicAck(long deliveryTag, boolean multiple) { 58 | acks.add(new Ack(deliveryTag, multiple)); 59 | } 60 | 61 | private static class PublishedMessage { 62 | private final String exchange; 63 | private final String routingKey; 64 | private final boolean mandatory; 65 | private final boolean immediate; 66 | private final AMQP.BasicProperties props; 67 | private final byte[] body; 68 | 69 | private PublishedMessage(String exchange, String routingKey, boolean mandatory, boolean immediate, AMQP.BasicProperties props, byte[] body) { 70 | this.exchange = exchange; 71 | this.routingKey = routingKey; 72 | this.mandatory = mandatory; 73 | this.immediate = immediate; 74 | this.props = props; 75 | this.body = body; 76 | } 77 | } 78 | 79 | private static class Reject { 80 | private final long deliveryTag; 81 | private final boolean requeue; 82 | 83 | private Reject(long deliveryTag, boolean requeue) { 84 | this.deliveryTag = deliveryTag; 85 | this.requeue = requeue; 86 | } 87 | } 88 | 89 | private static class Nack { 90 | private final long deliveryTag; 91 | private final boolean multiple; 92 | private final boolean requeue; 93 | 94 | private Nack(long deliveryTag, boolean multiple, boolean requeue) { 95 | this.deliveryTag = deliveryTag; 96 | this.multiple = multiple; 97 | this.requeue = requeue; 98 | } 99 | } 100 | 101 | private static class Ack { 102 | private final long deliveryTag; 103 | private final boolean multiple; 104 | 105 | private Ack(long deliveryTag, boolean multiple) { 106 | this.deliveryTag = deliveryTag; 107 | this.multiple = multiple; 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/TransactionalOperations.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock; 2 | 3 | import com.rabbitmq.client.AMQP; 4 | 5 | public interface TransactionalOperations { 6 | 7 | boolean basicPublish(String exchange, String routingKey, boolean mandatory, boolean immediate, AMQP.BasicProperties props, byte[] body); 8 | 9 | void basicReject(long deliveryTag, boolean requeue); 10 | 11 | void basicNack(long deliveryTag, boolean multiple, boolean requeue); 12 | 13 | void basicAck(long deliveryTag, boolean multiple); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/compatibility/MockConnectionFactoryFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.compatibility; 2 | 3 | import static com.github.fridujo.rabbitmq.mock.tool.Classes.missingClass; 4 | 5 | import com.github.fridujo.rabbitmq.mock.ConfigurableConnectionFactory; 6 | import com.github.fridujo.rabbitmq.mock.MockConnectionFactory; 7 | import com.rabbitmq.client.ConnectionFactory; 8 | 9 | /** 10 | * Factory building a mock implementation of {@link ConnectionFactory} according to the 11 | * version of **amqp-client** present in the classpath. 12 | */ 13 | public class MockConnectionFactoryFactory { 14 | 15 | public static ConfigurableConnectionFactory build() { 16 | return build(MockConnectionFactoryFactory.class.getClassLoader()); 17 | } 18 | 19 | public static ConfigurableConnectionFactory build(ClassLoader classLoader) { 20 | if (missingClass(classLoader, "com.rabbitmq.client.AddressResolver")) { 21 | // AddressResolver appears in version 3.6.6 of amqp-client 22 | // This execution branch is tested in spring-boot integration test with version 1.4.0.RELEASE 23 | return new MockConnectionFactoryWithoutAddressResolver(); 24 | } else { 25 | return new MockConnectionFactory(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/compatibility/MockConnectionFactoryWithoutAddressResolver.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.compatibility; 2 | 3 | import com.github.fridujo.rabbitmq.mock.ConfigurableConnectionFactory; 4 | import com.github.fridujo.rabbitmq.mock.MockConnection; 5 | import com.github.fridujo.rabbitmq.mock.MockNode; 6 | import com.github.fridujo.rabbitmq.mock.metrics.MetricsCollectorWrapper; 7 | import com.rabbitmq.client.Address; 8 | import com.rabbitmq.client.Connection; 9 | import com.rabbitmq.client.ConnectionFactory; 10 | 11 | import java.util.List; 12 | import java.util.concurrent.ExecutorService; 13 | 14 | public class MockConnectionFactoryWithoutAddressResolver extends ConfigurableConnectionFactory { 15 | 16 | public MockConnectionFactoryWithoutAddressResolver() { 17 | setAutomaticRecoveryEnabled(false); 18 | } 19 | 20 | public Connection newConnection(ExecutorService executor, List

addrs, String clientProvidedName) { 21 | return newConnection(); 22 | } 23 | 24 | public MockConnection newConnection() { 25 | MetricsCollectorWrapper metricsCollectorWrapper = MetricsCollectorWrapper.Builder.build(this); 26 | MockConnection mockConnection = new MockConnection(mockNode, metricsCollectorWrapper); 27 | return mockConnection; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/configuration/Configuration.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.configuration; 2 | 3 | import java.util.LinkedHashMap; 4 | import java.util.Map; 5 | 6 | import com.github.fridujo.rabbitmq.mock.exchange.MockExchangeCreator; 7 | import com.github.fridujo.rabbitmq.mock.exchange.TypedMockExchangeCreator; 8 | 9 | public class Configuration { 10 | private Map additionalExchangeCreatorsByType = new LinkedHashMap<>(); 11 | 12 | public Configuration registerAdditionalExchangeCreator(TypedMockExchangeCreator mockExchangeCreator) { 13 | additionalExchangeCreatorsByType.put(mockExchangeCreator.getType(), mockExchangeCreator); 14 | return this; 15 | } 16 | 17 | public MockExchangeCreator getAdditionalExchangeByType(String type) { 18 | return additionalExchangeCreatorsByType.get(type); 19 | } 20 | 21 | public boolean isAdditionalExchangeRegisteredFor(String type) { 22 | return additionalExchangeCreatorsByType.containsKey(type); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/configuration/QueueDeclarator.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.configuration; 2 | 3 | import java.io.IOException; 4 | import java.util.LinkedHashMap; 5 | import java.util.Map; 6 | 7 | import com.rabbitmq.client.AMQP; 8 | import com.rabbitmq.client.Channel; 9 | 10 | import com.github.fridujo.rabbitmq.mock.AmqArguments; 11 | import com.github.fridujo.rabbitmq.mock.MockChannel; 12 | 13 | public class QueueDeclarator { 14 | 15 | private final String queueName; 16 | private final Map queueArgs = new LinkedHashMap<>(); 17 | 18 | private QueueDeclarator(String queueName) { 19 | this.queueName = queueName; 20 | } 21 | 22 | public static QueueDeclarator queue(String queueName) { 23 | return new QueueDeclarator(queueName); 24 | } 25 | 26 | public static QueueDeclarator dynamicQueue() { 27 | return queue(""); 28 | } 29 | 30 | public QueueDeclarator withMessageTtl(long messageTtlInMs) { 31 | queueArgs.put(AmqArguments.MESSAGE_TTL_KEY, messageTtlInMs); 32 | return this; 33 | } 34 | 35 | 36 | public QueueDeclarator withDeadLetterExchange(String deadLetterExchange) { 37 | queueArgs.put(AmqArguments.DEAD_LETTER_EXCHANGE_KEY, deadLetterExchange); 38 | return this; 39 | } 40 | 41 | public QueueDeclarator withDeadLetterRoutingKey(String deadLetterRoutingKey) { 42 | queueArgs.put(AmqArguments.DEAD_LETTER_ROUTING_KEY_KEY, deadLetterRoutingKey); 43 | return this; 44 | } 45 | 46 | public QueueDeclarator withMaxLength(int maxLength) { 47 | queueArgs.put(AmqArguments.QUEUE_MAX_LENGTH_KEY, maxLength); 48 | return this; 49 | } 50 | 51 | public QueueDeclarator withMaxLengthBytes(int maxLengthBytes) { 52 | queueArgs.put(AmqArguments.QUEUE_MAX_LENGTH_BYTES_KEY, maxLengthBytes); 53 | return this; 54 | } 55 | 56 | public QueueDeclarator withOverflow(AmqArguments.Overflow overflow) { 57 | queueArgs.put(AmqArguments.OVERFLOW_KEY, overflow.toString()); 58 | return this; 59 | } 60 | 61 | public QueueDeclarator withMaxPriority(int maxPriority) { 62 | queueArgs.put(AmqArguments.MAX_PRIORITY_KEY, maxPriority); 63 | return this; 64 | } 65 | 66 | public AMQP.Queue.DeclareOk declare(Channel channel) throws IOException { 67 | return channel.queueDeclare(queueName, true, false, false, queueArgs); 68 | } 69 | 70 | public AMQP.Queue.DeclareOk declare(MockChannel channel) { 71 | return channel.queueDeclare(queueName, true, false, false, queueArgs); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/exchange/BindableMockExchange.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.exchange; 2 | 3 | import java.util.LinkedHashSet; 4 | import java.util.Map; 5 | import java.util.Objects; 6 | import java.util.Optional; 7 | import java.util.Set; 8 | import java.util.stream.Collectors; 9 | import java.util.stream.Stream; 10 | 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import com.github.fridujo.rabbitmq.mock.AmqArguments; 15 | import com.github.fridujo.rabbitmq.mock.MockQueue; 16 | import com.github.fridujo.rabbitmq.mock.Receiver; 17 | import com.github.fridujo.rabbitmq.mock.ReceiverPointer; 18 | import com.github.fridujo.rabbitmq.mock.ReceiverRegistry; 19 | import com.rabbitmq.client.AMQP; 20 | 21 | public abstract class BindableMockExchange implements MockExchange { 22 | private static final Logger LOGGER = LoggerFactory.getLogger(MockQueue.class); 23 | 24 | protected final Set bindConfigurations = new LinkedHashSet<>(); 25 | private final String name; 26 | private final String type; 27 | private final AmqArguments arguments; 28 | private final ReceiverPointer pointer; 29 | private final ReceiverRegistry receiverRegistry; 30 | 31 | protected BindableMockExchange(String name, String type, AmqArguments arguments, ReceiverRegistry receiverRegistry) { 32 | this.name = name; 33 | this.type = type; 34 | this.arguments = arguments; 35 | this.pointer = new ReceiverPointer(ReceiverPointer.Type.EXCHANGE, name); 36 | this.receiverRegistry = receiverRegistry; 37 | } 38 | 39 | @Override 40 | public String getType() { 41 | return type; 42 | } 43 | 44 | @Override 45 | public boolean publish(String previousExchangeName, String routingKey, AMQP.BasicProperties props, byte[] body) { 46 | Set matchingReceivers = matchingReceivers(routingKey, props) 47 | .map(receiverRegistry::getReceiver) 48 | .filter(Optional::isPresent) 49 | .map(Optional::get) 50 | .collect(Collectors.toSet()); 51 | 52 | if (matchingReceivers.isEmpty()) { 53 | return getAlternateExchange().map(e -> { 54 | LOGGER.debug(localized("message to alternate " + e)); 55 | return e.publish(name, routingKey, props, body); 56 | }).orElse(false); 57 | } else { 58 | matchingReceivers 59 | .forEach(e -> { 60 | LOGGER.debug(localized("message to " + e)); 61 | e.publish(name, routingKey, props, body); 62 | }); 63 | return true; 64 | } 65 | } 66 | 67 | private Optional getAlternateExchange() { 68 | return arguments.getAlternateExchange().flatMap(receiverRegistry::getReceiver); 69 | } 70 | 71 | protected abstract Stream matchingReceivers(String routingKey, AMQP.BasicProperties props); 72 | 73 | private String localized(String message) { 74 | return "[E " + name + "] " + message; 75 | } 76 | 77 | @Override 78 | public void bind(ReceiverPointer receiver, String routingKey, Map arguments) { 79 | bindConfigurations.add(new BindConfiguration(routingKey, receiver, arguments)); 80 | } 81 | 82 | @Override 83 | public void unbind(ReceiverPointer receiver, String routingKey, Map arguments) { 84 | bindConfigurations.remove(new BindConfiguration(routingKey, receiver, arguments)); 85 | } 86 | 87 | @Override 88 | public ReceiverPointer pointer() { 89 | return pointer; 90 | } 91 | 92 | @Override 93 | public String toString() { 94 | return getClass().getSimpleName() + " " + name; 95 | } 96 | 97 | public static class BindConfiguration { 98 | public final String bindingKey; 99 | public final ReceiverPointer receiverPointer; 100 | public final Map bindArguments; 101 | 102 | public BindConfiguration(String bindingKey, ReceiverPointer receiverPointer, Map bindArguments) { 103 | this.bindingKey = bindingKey; 104 | this.receiverPointer = receiverPointer; 105 | this.bindArguments = bindArguments; 106 | } 107 | 108 | public ReceiverPointer receiverPointer() { 109 | return receiverPointer; 110 | } 111 | 112 | @Override 113 | public boolean equals(Object o) { 114 | if (this == o) return true; 115 | if (o == null || getClass() != o.getClass()) return false; 116 | BindConfiguration that = (BindConfiguration) o; 117 | return Objects.equals(bindingKey, that.bindingKey) && 118 | Objects.equals(receiverPointer, that.receiverPointer) && 119 | Objects.equals(bindArguments, that.bindArguments); 120 | } 121 | 122 | @Override 123 | public int hashCode() { 124 | return Objects.hash(bindingKey, receiverPointer, bindArguments); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/exchange/ConsistentHashExchange.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.exchange; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.Optional; 7 | import java.util.stream.Collectors; 8 | import java.util.stream.Stream; 9 | 10 | import com.github.fridujo.rabbitmq.mock.AmqArguments; 11 | import com.github.fridujo.rabbitmq.mock.ReceiverPointer; 12 | import com.github.fridujo.rabbitmq.mock.ReceiverRegistry; 13 | import com.rabbitmq.client.AMQP; 14 | 15 | /** 16 | * Mimic the behavior of rabbitmq_consistent_hash_exchange. 17 | *

18 | * See https://github.com/rabbitmq/rabbitmq-consistent-hash-exchange. 19 | */ 20 | public class ConsistentHashExchange extends SingleReceiverExchange { 21 | 22 | public static final String TYPE = "x-consistent-hash"; 23 | private final List buckets = new ArrayList<>(); 24 | 25 | public ConsistentHashExchange(String name, AmqArguments arguments, ReceiverRegistry receiverRegistry) { 26 | super(name, TYPE, arguments, receiverRegistry); 27 | } 28 | 29 | @Override 30 | protected Optional selectReceiver(String routingKey, AMQP.BasicProperties props) { 31 | int bucketSelector = Math.abs(routingKey.hashCode()) % buckets.size(); 32 | return Optional.of(buckets.get(bucketSelector).receiverPointer); 33 | } 34 | 35 | @Override 36 | public void bind(ReceiverPointer receiver, String routingKey, Map arguments) { 37 | super.bind(receiver, routingKey, arguments); 38 | buckets.addAll(bucketsFor(routingKey, receiver)); 39 | } 40 | 41 | @Override 42 | public void unbind(ReceiverPointer receiver, String routingKey, Map arguments) { 43 | super.unbind(receiver, routingKey, arguments); 44 | buckets.removeIf(b -> b.receiverPointer.equals(receiver)); 45 | } 46 | 47 | /** 48 | * When a queue is bound to a Consistent Hash exchange, 49 | * the binding key is a number-as-a-string which indicates the binding weight: 50 | * the number of buckets (sections of the range) that will be associated with the target queue. 51 | *

52 | * The routing key is supposed to be an integer, {@code Object#hashCode} is used otherwise. 53 | */ 54 | private int routingKeyToWeight(String routingKey) { 55 | try { 56 | return Integer.parseInt(routingKey); 57 | } catch (NumberFormatException e) { 58 | return routingKey.hashCode(); 59 | } 60 | } 61 | 62 | private List bucketsFor(String routingKey, ReceiverPointer receiverPointer) { 63 | int weight = routingKeyToWeight(routingKey); 64 | return Stream.generate(() -> receiverPointer) 65 | .map(Bucket::new) 66 | .limit(weight) 67 | .collect(Collectors.toList()); 68 | } 69 | 70 | public static final class Bucket { 71 | private final ReceiverPointer receiverPointer; 72 | 73 | public Bucket(ReceiverPointer receiverPointer) { 74 | this.receiverPointer = receiverPointer; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/exchange/MockDefaultExchange.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.exchange; 2 | 3 | import java.util.Map; 4 | 5 | import com.rabbitmq.client.AMQP; 6 | 7 | import com.github.fridujo.rabbitmq.mock.MockNode; 8 | import com.github.fridujo.rabbitmq.mock.ReceiverPointer; 9 | 10 | public class MockDefaultExchange implements MockExchange { 11 | 12 | public static final String TYPE = "default"; 13 | public static final String NAME = ""; 14 | 15 | private final MockNode node; 16 | 17 | public MockDefaultExchange(MockNode mockNode) { 18 | this.node = mockNode; 19 | } 20 | 21 | @Override 22 | public boolean publish(String previousExchangeName, String routingKey, AMQP.BasicProperties props, byte[] body) { 23 | return node.getQueue(routingKey).map(q -> q.publish(NAME, routingKey, props, body)).orElse(false); 24 | } 25 | 26 | @Override 27 | public String getType() { 28 | return TYPE; 29 | } 30 | 31 | @Override 32 | public void bind(ReceiverPointer receiver, String routingKey, Map arguments) { 33 | // nothing needed 34 | } 35 | 36 | @Override 37 | public void unbind(ReceiverPointer pointer, String routingKey, Map arguments) { 38 | // nothing needed 39 | } 40 | 41 | @Override 42 | public ReceiverPointer pointer() { 43 | throw new IllegalStateException("No ReceiverPointer (internal use) should be needed for the default exchange"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/exchange/MockDirectExchange.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.exchange; 2 | 3 | import java.util.Map; 4 | 5 | import com.github.fridujo.rabbitmq.mock.AmqArguments; 6 | import com.github.fridujo.rabbitmq.mock.ReceiverRegistry; 7 | 8 | public class MockDirectExchange extends MultipleReceiverExchange { 9 | 10 | public static final String TYPE = "direct"; 11 | 12 | public MockDirectExchange(String name, AmqArguments arguments, ReceiverRegistry receiverRegistry) { 13 | super(name, TYPE, arguments, receiverRegistry); 14 | } 15 | 16 | @Override 17 | protected boolean match(BindConfiguration bindConfiguration, String routingKey, Map headers) { 18 | return bindConfiguration.bindingKey.equals(routingKey); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/exchange/MockExchange.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.exchange; 2 | 3 | import com.github.fridujo.rabbitmq.mock.Receiver; 4 | import com.github.fridujo.rabbitmq.mock.ReceiverPointer; 5 | 6 | import java.util.Map; 7 | 8 | public interface MockExchange extends Receiver { 9 | 10 | String getType(); 11 | 12 | void bind(ReceiverPointer mockQueue, String routingKey, Map arguments); 13 | 14 | void unbind(ReceiverPointer pointer, String routingKey, Map arguments); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/exchange/MockExchangeCreator.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.exchange; 2 | 3 | import com.github.fridujo.rabbitmq.mock.AmqArguments; 4 | import com.github.fridujo.rabbitmq.mock.ReceiverRegistry; 5 | 6 | @FunctionalInterface 7 | public interface MockExchangeCreator { 8 | 9 | static TypedMockExchangeCreator creatorWithExchangeType(String type, MockExchangeCreator creator) { 10 | return new TypedMockExchangeCreatorImpl(type, creator); 11 | } 12 | 13 | BindableMockExchange createMockExchange(String exchangeName, AmqArguments arguments, ReceiverRegistry receiverRegistry); 14 | 15 | public final class TypedMockExchangeCreatorImpl implements TypedMockExchangeCreator { 16 | 17 | private final String type; 18 | private final MockExchangeCreator creator; 19 | 20 | public TypedMockExchangeCreatorImpl(String type, MockExchangeCreator creator) { 21 | this.type = type; 22 | this.creator = creator; 23 | } 24 | 25 | @Override 26 | public String getType() { 27 | return type; 28 | } 29 | 30 | @Override 31 | public BindableMockExchange createMockExchange(String exchangeName, AmqArguments arguments, ReceiverRegistry receiverRegistry) { 32 | return creator.createMockExchange(exchangeName, arguments, receiverRegistry); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/exchange/MockExchangeFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.exchange; 2 | 3 | import com.github.fridujo.rabbitmq.mock.AmqArguments; 4 | import com.github.fridujo.rabbitmq.mock.ReceiverRegistry; 5 | import com.github.fridujo.rabbitmq.mock.configuration.Configuration; 6 | 7 | public class MockExchangeFactory { 8 | 9 | private final Configuration configuration; 10 | 11 | public MockExchangeFactory(Configuration configuration) { 12 | this.configuration = configuration; 13 | } 14 | 15 | public BindableMockExchange build(String exchangeName, 16 | String type, 17 | AmqArguments arguments, 18 | ReceiverRegistry receiverRegistry) { 19 | if (MockTopicExchange.TYPE.equals(type)) { 20 | return new MockTopicExchange(exchangeName, arguments, receiverRegistry); 21 | } else if (MockDirectExchange.TYPE.equals(type)) { 22 | return new MockDirectExchange(exchangeName, arguments, receiverRegistry); 23 | } else if (MockFanoutExchange.TYPE.equals(type)) { 24 | return new MockFanoutExchange(exchangeName, arguments, receiverRegistry); 25 | } else if (MockHeadersExchange.TYPE.equals(type)) { 26 | return new MockHeadersExchange(exchangeName, arguments, receiverRegistry); 27 | } else if (configuration.isAdditionalExchangeRegisteredFor(type)) { 28 | return configuration.getAdditionalExchangeByType(type) 29 | .createMockExchange(exchangeName, arguments, receiverRegistry); 30 | } 31 | throw new IllegalArgumentException("No exchange type " + type); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/exchange/MockFanoutExchange.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.exchange; 2 | 3 | import java.util.Map; 4 | 5 | import com.github.fridujo.rabbitmq.mock.AmqArguments; 6 | import com.github.fridujo.rabbitmq.mock.ReceiverRegistry; 7 | 8 | public class MockFanoutExchange extends MultipleReceiverExchange { 9 | 10 | public static final String TYPE = "fanout"; 11 | 12 | protected MockFanoutExchange(String name, AmqArguments arguments, ReceiverRegistry receiverRegistry) { 13 | super(name, TYPE, arguments, receiverRegistry); 14 | } 15 | 16 | @Override 17 | protected boolean match(BindConfiguration bindConfiguration, String routingKey, Map headers) { 18 | return true; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/exchange/MockHeadersExchange.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.exchange; 2 | 3 | import java.util.Arrays; 4 | import java.util.HashSet; 5 | import java.util.Map; 6 | import java.util.Objects; 7 | import java.util.Optional; 8 | import java.util.Set; 9 | import java.util.function.Predicate; 10 | import java.util.stream.Stream; 11 | 12 | import com.github.fridujo.rabbitmq.mock.AmqArguments; 13 | import com.github.fridujo.rabbitmq.mock.ReceiverRegistry; 14 | 15 | public class MockHeadersExchange extends MultipleReceiverExchange { 16 | 17 | public static final String TYPE = "headers"; 18 | 19 | private static final String MATCH_ALL = "all"; 20 | private static final Set X_MATCH_VALID_VALUES = new HashSet<>(Arrays.asList("any", MATCH_ALL)); 21 | 22 | public MockHeadersExchange(String exchangeName, AmqArguments arguments, ReceiverRegistry receiverRegistry) { 23 | super(exchangeName, TYPE, arguments, receiverRegistry); 24 | } 25 | 26 | @Override 27 | protected boolean match(BindConfiguration bindConfiguration, String routingKey, Map headers) { 28 | String xMatch = Optional.ofNullable(bindConfiguration.bindArguments.get(X_MATCH_KEY)) 29 | .filter(xMatchObject -> xMatchObject instanceof String) 30 | .map(String.class::cast) 31 | .filter(X_MATCH_VALID_VALUES::contains) 32 | .orElse(MATCH_ALL); 33 | 34 | Predicate> argumentPredicate = e -> e.getValue() == null ? headers.containsKey(e.getKey()) : Objects.equals(e.getValue(), headers.get(e.getKey())); 35 | Stream> argumentsToMatch = bindConfiguration.bindArguments.entrySet().stream() 36 | .filter(e -> !X_MATCH_KEY.equals(e.getKey())); 37 | 38 | final boolean match; 39 | if (MATCH_ALL.equals(xMatch)) { 40 | match = argumentsToMatch.allMatch(argumentPredicate); 41 | } else { 42 | match = argumentsToMatch.anyMatch(argumentPredicate); 43 | } 44 | return match; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/exchange/MockTopicExchange.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.exchange; 2 | 3 | import java.util.Map; 4 | 5 | import com.github.fridujo.rabbitmq.mock.AmqArguments; 6 | import com.github.fridujo.rabbitmq.mock.ReceiverRegistry; 7 | 8 | public class MockTopicExchange extends MultipleReceiverExchange { 9 | 10 | public static final String TYPE = "topic"; 11 | 12 | public MockTopicExchange(String name, AmqArguments arguments, ReceiverRegistry receiverRegistry) { 13 | super(name, TYPE, arguments, receiverRegistry); 14 | } 15 | 16 | /** 17 | * 18 | * https://www.rabbitmq.com/tutorials/tutorial-five-python.html 19 | * 20 | *

sharp / hash character substitutes to zero or more words

21 | *

An easy thought representation of routing keys is a list of words, words being separated by dots, 22 | * and topic exchange binding keys an description of this kind of list.

23 | *

Considering the key some.#.key.*, all these keys can match:

24 | *
    25 | *
  • some.key.redeyes where # matches for no words and * for 1 word (redeyes)
  • 26 | *
  • some.pink.key.blueeyes where # matches for 1 words (pink) and * for 1 word (blueeyes)
  • 27 | *
  • some.pink.rabbit.key.random where # matches for 2 words (pink, rabbit) and * for 1 word (random)
  • 28 | *
29 | */ 30 | protected boolean match(BindConfiguration bindConfiguration, String routingKey, Map headers) { 31 | String bindingRegex = bindConfiguration.bindingKey 32 | .replace("*", "([^\\.]+)") 33 | .replace(".#", "(\\.(.*))?") 34 | .replace("#", "(.+)"); 35 | return routingKey.matches(bindingRegex); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/exchange/MultipleReceiverExchange.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.exchange; 2 | 3 | import java.util.Map; 4 | import java.util.stream.Stream; 5 | 6 | import com.github.fridujo.rabbitmq.mock.AmqArguments; 7 | import com.github.fridujo.rabbitmq.mock.ReceiverPointer; 8 | import com.github.fridujo.rabbitmq.mock.ReceiverRegistry; 9 | import com.rabbitmq.client.AMQP; 10 | 11 | public abstract class MultipleReceiverExchange extends BindableMockExchange { 12 | 13 | protected MultipleReceiverExchange(String name, String type, AmqArguments arguments, ReceiverRegistry receiverRegistry) { 14 | super(name, type, arguments, receiverRegistry); 15 | } 16 | 17 | protected Stream matchingReceivers(String routingKey, AMQP.BasicProperties props) { 18 | return bindConfigurations 19 | .stream() 20 | .filter(bindConfiguration -> match(bindConfiguration, routingKey, props.getHeaders())) 21 | .map(BindableMockExchange.BindConfiguration::receiverPointer); 22 | } 23 | 24 | protected abstract boolean match(BindableMockExchange.BindConfiguration bindConfiguration, String routingKey, Map headers); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/exchange/SingleReceiverExchange.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.exchange; 2 | 3 | import java.util.Optional; 4 | import java.util.stream.Stream; 5 | 6 | import com.github.fridujo.rabbitmq.mock.AmqArguments; 7 | import com.github.fridujo.rabbitmq.mock.ReceiverPointer; 8 | import com.github.fridujo.rabbitmq.mock.ReceiverRegistry; 9 | import com.rabbitmq.client.AMQP; 10 | 11 | public abstract class SingleReceiverExchange extends BindableMockExchange { 12 | 13 | protected SingleReceiverExchange(String name, String type, AmqArguments arguments, ReceiverRegistry receiverRegistry) { 14 | super(name, type, arguments, receiverRegistry); 15 | } 16 | 17 | @Override 18 | protected Stream matchingReceivers(String routingKey, AMQP.BasicProperties props) { 19 | return Stream.of(selectReceiver(routingKey, props)) 20 | .filter(Optional::isPresent) 21 | .map(Optional::get); 22 | } 23 | 24 | protected abstract Optional selectReceiver(String routingKey, AMQP.BasicProperties props); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/exchange/TypedMockExchangeCreator.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.exchange; 2 | 3 | public interface TypedMockExchangeCreator extends MockExchangeCreator { 4 | 5 | String getType(); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/metrics/ImplementedMetricsCollectorWrapper.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.metrics; 2 | 3 | import com.rabbitmq.client.Channel; 4 | import com.rabbitmq.client.Connection; 5 | import com.rabbitmq.client.ConnectionFactory; 6 | import com.rabbitmq.client.MetricsCollector; 7 | import com.rabbitmq.client.NoOpMetricsCollector; 8 | 9 | public class ImplementedMetricsCollectorWrapper implements MetricsCollectorWrapper { 10 | 11 | private final ConnectionFactory connectionFactory; 12 | 13 | ImplementedMetricsCollectorWrapper(ConnectionFactory connectionFactory) { 14 | this.connectionFactory = connectionFactory; 15 | if (connectionFactory.getMetricsCollector() == null) { 16 | connectionFactory.setMetricsCollector(new NoOpMetricsCollector()); 17 | } 18 | } 19 | 20 | private MetricsCollector mc() { 21 | return connectionFactory.getMetricsCollector(); 22 | } 23 | 24 | @Override 25 | public void newConnection(Connection connection) { 26 | mc().newConnection(connection); 27 | } 28 | 29 | @Override 30 | public void closeConnection(Connection connection) { 31 | mc().closeConnection(connection); 32 | } 33 | 34 | @Override 35 | public void newChannel(Channel channel) { 36 | mc().newChannel(channel); 37 | } 38 | 39 | @Override 40 | public void closeChannel(Channel channel) { 41 | mc().closeChannel(channel); 42 | } 43 | 44 | @Override 45 | public void basicPublish(Channel channel) { 46 | mc().basicPublish(channel); 47 | } 48 | 49 | @Override 50 | public void consumedMessage(Channel channel, long deliveryTag, boolean autoAck) { 51 | mc().consumedMessage(channel, deliveryTag, autoAck); 52 | } 53 | 54 | @Override 55 | public void consumedMessage(Channel channel, long deliveryTag, String consumerTag) { 56 | mc().consumedMessage(channel, deliveryTag, consumerTag); 57 | } 58 | 59 | @Override 60 | public void basicAck(Channel channel, long deliveryTag, boolean multiple) { 61 | mc().basicAck(channel, deliveryTag, multiple); 62 | } 63 | 64 | @Override 65 | public void basicNack(Channel channel, long deliveryTag) { 66 | mc().basicNack(channel, deliveryTag); 67 | } 68 | 69 | @Override 70 | public void basicReject(Channel channel, long deliveryTag) { 71 | mc().basicReject(channel, deliveryTag); 72 | } 73 | 74 | @Override 75 | public void basicConsume(Channel channel, String consumerTag, boolean autoAck) { 76 | mc().basicConsume(channel, consumerTag, autoAck); 77 | } 78 | 79 | @Override 80 | public void basicCancel(Channel channel, String consumerTag) { 81 | mc().basicCancel(channel, consumerTag); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/metrics/MetricsCollectorWrapper.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.metrics; 2 | 3 | import com.rabbitmq.client.Channel; 4 | import com.rabbitmq.client.Connection; 5 | import com.rabbitmq.client.ConnectionFactory; 6 | 7 | import static com.github.fridujo.rabbitmq.mock.tool.Classes.missingClass; 8 | 9 | public interface MetricsCollectorWrapper { 10 | 11 | void newConnection(Connection connection); 12 | 13 | void closeConnection(Connection connection); 14 | 15 | void newChannel(Channel channel); 16 | 17 | void closeChannel(Channel channel); 18 | 19 | void basicPublish(Channel channel); 20 | 21 | void consumedMessage(Channel channel, long deliveryTag, boolean autoAck); 22 | 23 | void consumedMessage(Channel channel, long deliveryTag, String consumerTag); 24 | 25 | void basicAck(Channel channel, long deliveryTag, boolean multiple); 26 | 27 | void basicNack(Channel channel, long deliveryTag); 28 | 29 | void basicReject(Channel channel, long deliveryTag); 30 | 31 | void basicConsume(Channel channel, String consumerTag, boolean autoAck); 32 | 33 | void basicCancel(Channel channel, String consumerTag); 34 | 35 | class Builder { 36 | 37 | public static MetricsCollectorWrapper build(ConnectionFactory connectionFactory) { 38 | return build(MetricsCollectorWrapper.Builder.class.getClassLoader(), connectionFactory); 39 | } 40 | 41 | public static MetricsCollectorWrapper build(ClassLoader classLoader, ConnectionFactory connectionFactory) { 42 | final MetricsCollectorWrapper metricsCollectorWrapper; 43 | if (missingClass(classLoader, "com.rabbitmq.client.MetricsCollector")) { 44 | metricsCollectorWrapper = new NoopMetricsCollectorWrapper(); 45 | 46 | } else { 47 | metricsCollectorWrapper = new ImplementedMetricsCollectorWrapper(connectionFactory); 48 | } 49 | return metricsCollectorWrapper; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/metrics/NoopMetricsCollectorWrapper.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.metrics; 2 | 3 | import com.rabbitmq.client.Channel; 4 | import com.rabbitmq.client.Connection; 5 | 6 | public class NoopMetricsCollectorWrapper implements MetricsCollectorWrapper { 7 | @Override 8 | public void newConnection(Connection connection) { 9 | // no implementation 10 | } 11 | 12 | @Override 13 | public void closeConnection(Connection connection) { 14 | // no implementation 15 | } 16 | 17 | @Override 18 | public void newChannel(Channel channel) { 19 | // no implementation 20 | } 21 | 22 | @Override 23 | public void closeChannel(Channel channel) { 24 | // no implementation 25 | } 26 | 27 | @Override 28 | public void basicPublish(Channel channel) { 29 | // no implementation 30 | } 31 | 32 | @Override 33 | public void consumedMessage(Channel channel, long deliveryTag, boolean autoAck) { 34 | // no implementation 35 | } 36 | 37 | @Override 38 | public void consumedMessage(Channel channel, long deliveryTag, String consumerTag) { 39 | // no implementation 40 | } 41 | 42 | @Override 43 | public void basicAck(Channel channel, long deliveryTag, boolean multiple) { 44 | // no implementation 45 | } 46 | 47 | @Override 48 | public void basicNack(Channel channel, long deliveryTag) { 49 | // no implementation 50 | } 51 | 52 | @Override 53 | public void basicReject(Channel channel, long deliveryTag) { 54 | // no implementation 55 | } 56 | 57 | @Override 58 | public void basicConsume(Channel channel, String consumerTag, boolean autoAck) { 59 | // no implementation 60 | } 61 | 62 | @Override 63 | public void basicCancel(Channel channel, String consumerTag) { 64 | // no implementation 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/tool/Classes.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.tool; 2 | 3 | public abstract class Classes { 4 | private Classes() { 5 | } 6 | 7 | public static boolean missingClass(ClassLoader classLoader, String className) { 8 | try { 9 | classLoader.loadClass(className); 10 | return false; 11 | } catch (ClassNotFoundException e) { 12 | return true; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/tool/Exceptions.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.tool; 2 | 3 | import java.util.function.Function; 4 | 5 | public class Exceptions { 6 | 7 | public static void runAndEatExceptions(ThrowingRunnable throwingRunnable) { 8 | try { 9 | throwingRunnable.run(); 10 | } catch (Exception unused) { 11 | // we are not interested in this error 12 | } 13 | } 14 | 15 | public static void runAndTransformExceptions(ThrowingRunnable throwingRunnable, 16 | Function exceptionMapper) throws T { 17 | try { 18 | throwingRunnable.run(); 19 | } catch (Exception original) { 20 | throw exceptionMapper.apply(original); 21 | } 22 | } 23 | 24 | public interface ThrowingRunnable { 25 | void run() throws Exception; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/tool/NamedThreadFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.tool; 2 | 3 | import java.util.concurrent.ThreadFactory; 4 | import java.util.concurrent.atomic.AtomicInteger; 5 | import java.util.function.Function; 6 | 7 | public class NamedThreadFactory implements ThreadFactory { 8 | 9 | private final ThreadGroup group; 10 | private final AtomicInteger threadNumber = new AtomicInteger(1); 11 | private final Function nameCreator; 12 | 13 | public NamedThreadFactory(Function nameCreator) { 14 | this.nameCreator = nameCreator; 15 | SecurityManager s = System.getSecurityManager(); 16 | group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); 17 | } 18 | 19 | @Override 20 | public Thread newThread(Runnable r) { 21 | return new Thread(group, r, nameCreator.apply(threadNumber.getAndIncrement()), 0); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/github/fridujo/rabbitmq/mock/tool/RestartableExecutorService.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.tool; 2 | 3 | import java.util.Collection; 4 | import java.util.List; 5 | import java.util.concurrent.Callable; 6 | import java.util.concurrent.ExecutionException; 7 | import java.util.concurrent.ExecutorService; 8 | import java.util.concurrent.Future; 9 | import java.util.concurrent.TimeUnit; 10 | import java.util.concurrent.TimeoutException; 11 | import java.util.function.Supplier; 12 | 13 | public class RestartableExecutorService implements ExecutorService { 14 | 15 | private final Supplier executorServiceFactory; 16 | private ExecutorService delegate; 17 | 18 | public RestartableExecutorService(Supplier executorServiceFactory) { 19 | this.executorServiceFactory = executorServiceFactory; 20 | this.delegate = executorServiceFactory.get(); 21 | } 22 | 23 | @Override 24 | public void shutdown() { 25 | delegate.shutdown(); 26 | } 27 | 28 | @Override 29 | public List shutdownNow() { 30 | return delegate.shutdownNow(); 31 | } 32 | 33 | @Override 34 | public boolean isShutdown() { 35 | return delegate.isShutdown(); 36 | } 37 | 38 | @Override 39 | public boolean isTerminated() { 40 | return delegate.isTerminated(); 41 | } 42 | 43 | @Override 44 | public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { 45 | return delegate.awaitTermination(timeout, unit); 46 | } 47 | 48 | @Override 49 | public Future submit(Callable task) { 50 | return delegate.submit(task); 51 | } 52 | 53 | @Override 54 | public Future submit(Runnable task, T result) { 55 | return delegate.submit(task, result); 56 | } 57 | 58 | @Override 59 | public Future submit(Runnable task) { 60 | return delegate.submit(task); 61 | } 62 | 63 | @Override 64 | public List> invokeAll(Collection> tasks) throws InterruptedException { 65 | return delegate.invokeAll(tasks); 66 | } 67 | 68 | @Override 69 | public List> invokeAll(Collection> tasks, long timeout, TimeUnit unit) throws InterruptedException { 70 | return delegate.invokeAll(tasks, timeout, unit); 71 | } 72 | 73 | @Override 74 | public T invokeAny(Collection> tasks) throws InterruptedException, ExecutionException { 75 | return delegate.invokeAny(tasks); 76 | } 77 | 78 | @Override 79 | public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { 80 | return delegate.invokeAny(tasks, timeout, unit); 81 | } 82 | 83 | @Override 84 | public void execute(Runnable command) { 85 | delegate.execute(command); 86 | } 87 | 88 | public synchronized void restart() { 89 | this.delegate = executorServiceFactory.get(); 90 | } 91 | 92 | public ExecutorService getDelegate() { 93 | return delegate; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/test/java/com/github/fridujo/rabbitmq/mock/ComplexUseCasesTests.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock; 2 | 3 | import static com.github.fridujo.rabbitmq.mock.configuration.QueueDeclarator.queue; 4 | import static com.github.fridujo.rabbitmq.mock.tool.Exceptions.runAndEatExceptions; 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | import java.io.IOException; 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.Collections; 11 | import java.util.List; 12 | import java.util.concurrent.Semaphore; 13 | import java.util.concurrent.TimeUnit; 14 | import java.util.concurrent.TimeoutException; 15 | 16 | import org.junit.jupiter.api.Test; 17 | 18 | import com.github.fridujo.rabbitmq.mock.configuration.QueueDeclarator; 19 | import com.rabbitmq.client.AMQP; 20 | import com.rabbitmq.client.BuiltinExchangeType; 21 | import com.rabbitmq.client.Channel; 22 | import com.rabbitmq.client.Connection; 23 | import com.rabbitmq.client.DefaultConsumer; 24 | import com.rabbitmq.client.Envelope; 25 | import com.rabbitmq.client.GetResponse; 26 | 27 | class ComplexUseCasesTests { 28 | 29 | @Test 30 | void expired_message_should_be_consumable_after_being_dead_lettered() throws IOException, TimeoutException, InterruptedException { 31 | try (Connection conn = new MockConnectionFactory().newConnection()) { 32 | try (Channel channel = conn.createChannel()) { 33 | channel.exchangeDeclare("rejected-ex", BuiltinExchangeType.FANOUT); 34 | channel.queueDeclare("rejected", true, false, false, null); 35 | channel.queueBindNoWait("rejected", "rejected-ex", "unused", null); 36 | queue("fruits").withMessageTtl(10L).withDeadLetterExchange("rejected-ex").declare(channel); 37 | 38 | List messages = new ArrayList<>(); 39 | channel.basicConsume("rejected", new DefaultConsumer(channel) { 40 | @Override 41 | public void handleDelivery(String consumerTag, 42 | Envelope envelope, 43 | AMQP.BasicProperties properties, 44 | byte[] body) { 45 | messages.add(new String(body)); 46 | } 47 | }); 48 | 49 | channel.basicPublish("", "fruits", null, "banana".getBytes()); 50 | TimeUnit.MILLISECONDS.sleep(100L); 51 | assertThat(messages).hasSize(1); 52 | } 53 | } 54 | } 55 | 56 | @Test 57 | void multiple_expired_messages_are_not_delivered_to_consumer() throws IOException, TimeoutException, InterruptedException { 58 | try (Connection conn = new MockConnectionFactory().newConnection()) { 59 | try (Channel channel = conn.createChannel()) { 60 | queue("fruits").withMessageTtl(-1L).declare(channel); 61 | 62 | List messages = new ArrayList<>(); 63 | channel.basicConsume("fruits", new DefaultConsumer(channel) { 64 | @Override 65 | public void handleDelivery(String consumerTag, 66 | Envelope envelope, 67 | AMQP.BasicProperties properties, 68 | byte[] body) { 69 | messages.add(new String(body)); 70 | } 71 | }); 72 | 73 | channel.basicPublish("", "fruits", null, "banana".getBytes()); 74 | channel.basicPublish("", "fruits", null, "orange".getBytes()); 75 | TimeUnit.MILLISECONDS.sleep(100L); 76 | assertThat(messages).hasSize(0); 77 | } 78 | } 79 | } 80 | 81 | @Test 82 | void expired_message_should_be_consumable_after_being_dead_lettered_with_ttl_per_message() throws IOException, TimeoutException, InterruptedException { 83 | try (Connection conn = new MockConnectionFactory().newConnection()) { 84 | try (Channel channel = conn.createChannel()) { 85 | channel.exchangeDeclare("rejected-ex", BuiltinExchangeType.FANOUT); 86 | queue("rejected").declare(channel); 87 | channel.queueBindNoWait("rejected", "rejected-ex", "unused", null); 88 | queue("fruits").withDeadLetterExchange("rejected-ex").declare(channel); 89 | 90 | List messages = new ArrayList<>(); 91 | channel.basicConsume("rejected", new DefaultConsumer(channel) { 92 | @Override 93 | public void handleDelivery(String consumerTag, 94 | Envelope envelope, 95 | AMQP.BasicProperties properties, 96 | byte[] body) { 97 | messages.add(new String(body)); 98 | } 99 | }); 100 | 101 | AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder() 102 | .expiration("50") 103 | .build(); 104 | channel.basicPublish("", "fruits", properties, "banana".getBytes()); 105 | TimeUnit.MILLISECONDS.sleep(150L); 106 | assertThat(messages).hasSize(1); 107 | } 108 | } 109 | } 110 | 111 | @Test 112 | void basicGet_expired_message_while_consumer_is_active() throws IOException, TimeoutException, InterruptedException { 113 | try (Connection conn = new MockConnectionFactory().newConnection()) { 114 | Semaphore consuming = new Semaphore(0); 115 | try (Channel channel = conn.createChannel()) { 116 | queue("fruits").declare(channel); 117 | 118 | channel.basicConsume("fruits", new DefaultConsumer(channel) { 119 | @Override 120 | public void handleDelivery(String consumerTag, 121 | Envelope envelope, 122 | AMQP.BasicProperties properties, 123 | byte[] body) { 124 | runAndEatExceptions(consuming::acquire); // will consume banana 125 | } 126 | }); 127 | 128 | AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder() 129 | .expiration("50") 130 | .build(); 131 | channel.basicPublish("", "fruits", null, "banana".getBytes()); 132 | channel.basicPublish("", "fruits", properties, "kiwi".getBytes()); 133 | 134 | TimeUnit.MILLISECONDS.sleep(80L); 135 | 136 | GetResponse kiwiMessage = channel.basicGet("fruits", true); 137 | assertThat(kiwiMessage).isNull(); // dead-lettered but not by the queue event-loop 138 | consuming.release(); 139 | } 140 | } 141 | } 142 | 143 | @Test 144 | void can_consume_messages_published_in_a_previous_connection() throws InterruptedException { 145 | MockConnectionFactory connectionFactory = new MockConnectionFactory(); 146 | try (MockConnection conn = connectionFactory.newConnection()) { 147 | try (MockChannel channel = conn.createChannel()) { 148 | queue("numbers").declare(channel); 149 | Arrays.asList("one", "two", "three").stream().forEach(message -> 150 | channel.basicPublish("", "numbers", null, message.getBytes()) 151 | ); 152 | } 153 | } 154 | 155 | try (MockConnection conn = connectionFactory.newConnection()) { 156 | try (MockChannel channel = conn.createChannel()) { 157 | 158 | List messages = new ArrayList<>(); 159 | Semaphore deliveries = new Semaphore(-2); 160 | 161 | channel.basicConsume("numbers", new DefaultConsumer(channel) { 162 | @Override 163 | public void handleDelivery(String consumerTag, 164 | Envelope envelope, 165 | AMQP.BasicProperties properties, 166 | byte[] body) { 167 | messages.add(new String(body)); 168 | deliveries.release(); 169 | } 170 | }); 171 | 172 | assertThat(deliveries.tryAcquire(1, TimeUnit.SECONDS)).as("Messages have been delivered").isTrue(); 173 | 174 | assertThat(messages).containsExactly("one", "two", "three"); 175 | } 176 | } 177 | } 178 | 179 | @Test 180 | void closing_one_connection_does_not_cancel_consumers_defined_within_another_one() throws InterruptedException { 181 | MockConnectionFactory connectionFactory = new MockConnectionFactory(); 182 | try (MockConnection conn = connectionFactory.newConnection()) { 183 | try (MockChannel channel = conn.createChannel()) { 184 | queue("numbers").declare(channel); 185 | 186 | List messages = new ArrayList<>(); 187 | Semaphore deliveries = new Semaphore(-2); 188 | 189 | channel.basicConsume("numbers", new DefaultConsumer(channel) { 190 | @Override 191 | public void handleDelivery(String consumerTag, 192 | Envelope envelope, 193 | AMQP.BasicProperties properties, 194 | byte[] body) { 195 | messages.add(new String(body)); 196 | deliveries.release(); 197 | } 198 | }); 199 | 200 | connectionFactory.newConnection().close(); 201 | 202 | try (MockConnection conn2 = connectionFactory.newConnection()) { 203 | try (MockChannel channel2 = conn.createChannel()) { 204 | Arrays.asList("one", "two", "three").stream().forEach(message -> 205 | channel2.basicPublish("", "numbers", null, message.getBytes()) 206 | ); 207 | } 208 | } 209 | 210 | assertThat(deliveries.tryAcquire(1, TimeUnit.SECONDS)).as("Messages have been delivered").isTrue(); 211 | 212 | assertThat(messages).containsExactly("one", "two", "three"); 213 | } 214 | } 215 | } 216 | 217 | @Test 218 | void unacked_messages_are_made_available_anew_when_consumer_is_cancelled() throws InterruptedException { 219 | try (MockConnection conn = new MockConnectionFactory().newConnection()) { 220 | try (MockChannel channel = conn.createChannel()) { 221 | String queueName = QueueDeclarator.dynamicQueue().declare(channel).getQueue(); 222 | Arrays.asList("one", "two", "three") 223 | .forEach(message -> channel.basicPublish("", queueName, null, message.getBytes())); 224 | 225 | Semaphore deliveries = new Semaphore(-2); 226 | String consumerTag = channel.basicConsume(queueName, false, Collections.emptyMap(), new DefaultConsumer(channel) { 227 | @Override 228 | public void handleDelivery(String consumerTag, 229 | Envelope envelope, 230 | AMQP.BasicProperties properties, 231 | byte[] body) { 232 | if ("one".equals(new String(body))) { 233 | channel.basicAck(envelope.getDeliveryTag(), false); 234 | } 235 | deliveries.release(); 236 | } 237 | }); 238 | 239 | assertThat(deliveries.tryAcquire(1, TimeUnit.SECONDS)).as("Messages have been delivered").isTrue(); 240 | 241 | channel.basicCancel(consumerTag); 242 | 243 | List messages = new ArrayList<>(); 244 | Semaphore deliveriesRoundTwo = new Semaphore(-1); 245 | channel.basicConsume(queueName, false, Collections.emptyMap(), new DefaultConsumer(channel) { 246 | @Override 247 | public void handleDelivery(String consumerTag, 248 | Envelope envelope, 249 | AMQP.BasicProperties properties, 250 | byte[] body) { 251 | messages.add(new String(body)); 252 | deliveriesRoundTwo.release(); 253 | } 254 | }); 255 | 256 | assertThat(deliveriesRoundTwo.tryAcquire(1, TimeUnit.SECONDS)).as("Messages have been delivered (round 2)").isTrue(); 257 | assertThat(messages).containsExactly("two", "three"); 258 | } 259 | } 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/test/java/com/github/fridujo/rabbitmq/mock/IntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock; 2 | 3 | import static com.github.fridujo.rabbitmq.mock.configuration.QueueDeclarator.queue; 4 | import static com.github.fridujo.rabbitmq.mock.tool.Exceptions.runAndEatExceptions; 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | import static org.assertj.core.api.Assertions.fail; 7 | 8 | import java.io.IOException; 9 | import java.util.ArrayList; 10 | import java.util.HashMap; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.concurrent.CountDownLatch; 14 | import java.util.concurrent.Semaphore; 15 | import java.util.concurrent.TimeUnit; 16 | import java.util.concurrent.TimeoutException; 17 | import java.util.concurrent.atomic.AtomicInteger; 18 | import java.util.concurrent.atomic.AtomicReference; 19 | 20 | import com.rabbitmq.client.AMQP; 21 | import com.rabbitmq.client.Channel; 22 | import com.rabbitmq.client.Connection; 23 | import com.rabbitmq.client.DefaultConsumer; 24 | import com.rabbitmq.client.Envelope; 25 | import com.rabbitmq.client.GetResponse; 26 | import org.junit.jupiter.api.Test; 27 | 28 | class IntegrationTest { 29 | 30 | @Test 31 | void basic_consume_case() throws IOException, TimeoutException, InterruptedException { 32 | String exchangeName = "test-exchange"; 33 | String routingKey = "test.key"; 34 | 35 | try (Connection conn = new MockConnectionFactory().newConnection()) { 36 | assertThat(conn).isInstanceOf(MockConnection.class); 37 | 38 | try (Channel channel = conn.createChannel()) { 39 | assertThat(channel).isInstanceOf(MockChannel.class); 40 | 41 | channel.exchangeDeclare(exchangeName, "direct", true); 42 | String queueName = channel.queueDeclare().getQueue(); 43 | channel.queueBind(queueName, exchangeName, routingKey); 44 | 45 | List messages = new ArrayList<>(); 46 | channel.basicConsume(queueName, false, "myConsumerTag", 47 | new DefaultConsumer(channel) { 48 | @Override 49 | public void handleDelivery(String consumerTag, 50 | Envelope envelope, 51 | AMQP.BasicProperties properties, 52 | byte[] body) throws IOException { 53 | long deliveryTag = envelope.getDeliveryTag(); 54 | messages.add(new String(body)); 55 | // (process the message components here ...) 56 | channel.basicAck(deliveryTag, false); 57 | } 58 | }); 59 | 60 | byte[] messageBodyBytes = "Hello, world!".getBytes(); 61 | channel.basicPublish(exchangeName, routingKey, null, messageBodyBytes); 62 | 63 | TimeUnit.MILLISECONDS.sleep(200L); 64 | 65 | assertThat(messages).containsExactly("Hello, world!"); 66 | } 67 | } 68 | } 69 | 70 | @Test 71 | void basic_get_case() throws IOException, TimeoutException { 72 | String exchangeName = "test-exchange"; 73 | String routingKey = "test.key"; 74 | 75 | try (Connection conn = new MockConnectionFactory().newConnection()) { 76 | assertThat(conn).isInstanceOf(MockConnection.class); 77 | 78 | try (Channel channel = conn.createChannel()) { 79 | assertThat(channel).isInstanceOf(MockChannel.class); 80 | 81 | channel.exchangeDeclare(exchangeName, "direct", true); 82 | String queueName = channel.queueDeclare().getQueue(); 83 | channel.queueBind(queueName, exchangeName, routingKey); 84 | 85 | byte[] messageBodyBytes = "Hello, world!".getBytes(); 86 | channel.basicPublish(exchangeName, routingKey, null, messageBodyBytes); 87 | 88 | GetResponse response = channel.basicGet(queueName, false); 89 | if (response == null) { 90 | fail("AMQP GetReponse must not be null"); 91 | } else { 92 | byte[] body = response.getBody(); 93 | assertThat(new String(body)).isEqualTo("Hello, world!"); 94 | long deliveryTag = response.getEnvelope().getDeliveryTag(); 95 | 96 | channel.basicAck(deliveryTag, false); 97 | } 98 | } 99 | } 100 | } 101 | 102 | @Test 103 | void basic_consume_nack_case() throws IOException, TimeoutException, InterruptedException { 104 | String exchangeName = "test-exchange"; 105 | String routingKey = "test.key"; 106 | 107 | AtomicInteger atomicInteger = new AtomicInteger(); 108 | final Semaphore waitForAtLeastOneDelivery = new Semaphore(0); 109 | final Semaphore waitForCancellation = new Semaphore(0); 110 | 111 | try (Connection conn = new MockConnectionFactory().newConnection()) { 112 | assertThat(conn).isInstanceOf(MockConnection.class); 113 | 114 | try (Channel channel = conn.createChannel()) { 115 | assertThat(channel).isInstanceOf(MockChannel.class); 116 | 117 | channel.exchangeDeclare(exchangeName, "direct", true); 118 | String queueName = channel.queueDeclare().getQueue(); 119 | channel.queueBind(queueName, exchangeName, routingKey); 120 | 121 | channel.basicConsume(queueName, false, "myConsumerTag", 122 | new DefaultConsumer(channel) { 123 | @Override 124 | public void handleDelivery(String consumerTag, 125 | Envelope envelope, 126 | AMQP.BasicProperties properties, 127 | byte[] body) throws IOException { 128 | waitForAtLeastOneDelivery.release(); 129 | long deliveryTag = envelope.getDeliveryTag(); 130 | atomicInteger.incrementAndGet(); 131 | channel.basicNack(deliveryTag, false, true); 132 | } 133 | 134 | @Override 135 | public void handleCancel(String consumerTag) { 136 | waitForCancellation.release(); 137 | } 138 | }); 139 | 140 | byte[] messageBodyBytes = "Hello, world!".getBytes(); 141 | channel.basicPublish(exchangeName, routingKey, null, messageBodyBytes); 142 | waitForAtLeastOneDelivery.acquire(); 143 | } 144 | } 145 | 146 | // WHEN after closing the connection and resetting the counter 147 | atomicInteger.set(0); 148 | 149 | waitForCancellation.acquire(); 150 | assertThat(atomicInteger.get()) 151 | .describedAs("After connection closed, and Consumer cancellation, no message should be delivered anymore") 152 | .isZero(); 153 | } 154 | 155 | @Test 156 | void redelivered_message_should_have_redelivery_marked_as_true() throws IOException, TimeoutException, InterruptedException { 157 | try (Connection conn = new MockConnectionFactory().newConnection()) { 158 | CountDownLatch messagesToBeProcessed = new CountDownLatch(2); 159 | try (Channel channel = conn.createChannel()) { 160 | queue("fruits").declare(channel); 161 | AtomicReference redeliveredMessageEnvelope = new AtomicReference(); 162 | 163 | channel.basicConsume("fruits", new DefaultConsumer(channel) { 164 | @Override 165 | public void handleDelivery(String consumerTag, 166 | Envelope envelope, 167 | AMQP.BasicProperties properties, 168 | byte[] body) { 169 | if(messagesToBeProcessed.getCount() == 1){ 170 | redeliveredMessageEnvelope.set(envelope); 171 | runAndEatExceptions(messagesToBeProcessed::countDown); 172 | 173 | }else{ 174 | runAndEatExceptions(() -> channel.basicNack(envelope.getDeliveryTag(), false, true)); 175 | runAndEatExceptions(messagesToBeProcessed::countDown); 176 | } 177 | 178 | } 179 | }); 180 | 181 | channel.basicPublish("", "fruits", null, "banana".getBytes()); 182 | 183 | final boolean finishedProperly = messagesToBeProcessed.await(1000, TimeUnit.SECONDS); 184 | assertThat(finishedProperly).isTrue(); 185 | assertThat(redeliveredMessageEnvelope.get()).isNotNull(); 186 | assertThat(redeliveredMessageEnvelope.get().isRedeliver()).isTrue(); 187 | } 188 | } 189 | } 190 | 191 | @Test 192 | void basic_consume_with_multiple_bindings_should_receive_messages_via_all_bindings() throws IOException, TimeoutException, InterruptedException { 193 | String exchangeName = "test-exchange"; 194 | 195 | try (Connection conn = new MockConnectionFactory().newConnection()) { 196 | assertThat(conn).isInstanceOf(MockConnection.class); 197 | 198 | try (Channel channel = conn.createChannel()) { 199 | assertThat(channel).isInstanceOf(MockChannel.class); 200 | 201 | channel.exchangeDeclare(exchangeName, "headers", true); 202 | String queueName = channel.queueDeclare().getQueue(); 203 | 204 | // Create multiple bindings that only differ in header parameter value: 205 | channel.queueBind(queueName, exchangeName, "", args("my-header", "x", "x-match", "all")); 206 | channel.queueBind(queueName, exchangeName, "", args("my-header", "y", "x-match", "all")); 207 | channel.queueBind(queueName, exchangeName, "", args("my-header", "z", "x-match", "all")); 208 | 209 | List messages = new ArrayList<>(); 210 | channel.basicConsume(queueName, false, "", 211 | new DefaultConsumer(channel) { 212 | @Override 213 | public void handleDelivery(String consumerTag, 214 | Envelope envelope, 215 | AMQP.BasicProperties properties, 216 | byte[] body) throws IOException { 217 | long deliveryTag = envelope.getDeliveryTag(); 218 | messages.add(new String(body)); 219 | // (process the message components here ...) 220 | channel.basicAck(deliveryTag, false); 221 | } 222 | }); 223 | 224 | // publish messages that only differ in header parameter value: 225 | channel.basicPublish(exchangeName, "", new AMQP.BasicProperties.Builder().headers(args("my-header", "x")).build(), "Hello, world!".getBytes()); 226 | channel.basicPublish(exchangeName, "", new AMQP.BasicProperties.Builder().headers(args("my-header", "y")).build(), "How are you, world!".getBytes()); 227 | channel.basicPublish(exchangeName, "", new AMQP.BasicProperties.Builder().headers(args("my-header", "z")).build(), "Goodbye, world!".getBytes()); 228 | 229 | TimeUnit.MILLISECONDS.sleep(200L); 230 | 231 | // expect three messages: 232 | assertThat(messages).containsExactly("Hello, world!", "How are you, world!", "Goodbye, world!"); 233 | 234 | // remove one of the bindings: 235 | channel.queueUnbind(queueName, exchangeName, "", args("my-header", "y", "x-match", "all")); 236 | messages.clear(); 237 | 238 | // publish messages that only differ in header parameter value: 239 | channel.basicPublish(exchangeName, "", new AMQP.BasicProperties.Builder().headers(args("my-header", "x")).build(), "Hello, world!".getBytes()); 240 | channel.basicPublish(exchangeName, "", new AMQP.BasicProperties.Builder().headers(args("my-header", "y")).build(), "How are you, world!".getBytes()); 241 | channel.basicPublish(exchangeName, "", new AMQP.BasicProperties.Builder().headers(args("my-header", "z")).build(), "Goodbye, world!".getBytes()); 242 | 243 | TimeUnit.MILLISECONDS.sleep(200L); 244 | 245 | // expect two messages from remaining bindings: 246 | assertThat(messages).containsExactly("Hello, world!", "Goodbye, world!"); 247 | } 248 | } 249 | } 250 | 251 | private static Map args(Object... args) { 252 | if (args.length % 2 != 0) { 253 | throw new IllegalArgumentException("must provide arguments in pairs"); 254 | } 255 | 256 | Map map = new HashMap<>(); 257 | 258 | for (int i = 0; i < args.length; i += 2) { 259 | if (!(args[i] instanceof String)) { 260 | throw new IllegalArgumentException("argument " + i + " must be a String: " + args[i]); 261 | } 262 | 263 | map.put((String)args[i], args[i + 1]); 264 | } 265 | 266 | return map; 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/test/java/com/github/fridujo/rabbitmq/mock/MetricsCollectorTest.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock; 2 | 3 | import com.rabbitmq.client.AMQP; 4 | import com.rabbitmq.client.Channel; 5 | import com.rabbitmq.client.DefaultConsumer; 6 | import com.rabbitmq.client.Envelope; 7 | import com.rabbitmq.client.GetResponse; 8 | import com.rabbitmq.client.impl.MicrometerMetricsCollector; 9 | import io.micrometer.core.instrument.simple.SimpleMeterRegistry; 10 | import org.awaitility.Awaitility; 11 | import org.junit.jupiter.api.Assertions; 12 | import org.junit.jupiter.api.Test; 13 | 14 | import java.io.IOException; 15 | import java.time.Duration; 16 | import java.util.concurrent.TimeUnit; 17 | import java.util.concurrent.TimeoutException; 18 | import java.util.concurrent.atomic.AtomicBoolean; 19 | import java.util.function.Supplier; 20 | 21 | import static org.assertj.core.api.Assertions.assertThat; 22 | 23 | public class MetricsCollectorTest { 24 | 25 | @Test 26 | void metrics_collector_is_invoked_on_connection_creation_and_closing() { 27 | MockConnectionFactory mockConnectionFactory = new MockConnectionFactory(); 28 | SimpleMeterRegistry registry = new SimpleMeterRegistry(); 29 | mockConnectionFactory.setMetricsCollector(new MicrometerMetricsCollector(registry)); 30 | 31 | assertThat(registry.get("rabbitmq.connections").gauge().value()).isEqualTo(0); 32 | try (MockConnection connection = mockConnectionFactory.newConnection()) { 33 | assertThat(registry.get("rabbitmq.connections").gauge().value()).isEqualTo(1); 34 | } 35 | assertThat(registry.get("rabbitmq.connections").gauge().value()).isEqualTo(0); 36 | } 37 | 38 | @Test 39 | void metrics_collector_is_invoked_on_channel_creation_and_closing() throws IOException, TimeoutException { 40 | MockConnectionFactory mockConnectionFactory = new MockConnectionFactory(); 41 | SimpleMeterRegistry registry = new SimpleMeterRegistry(); 42 | mockConnectionFactory.setMetricsCollector(new MicrometerMetricsCollector(registry)); 43 | 44 | try (MockConnection connection = mockConnectionFactory.newConnection()) { 45 | assertThat(registry.get("rabbitmq.channels").gauge().value()).isEqualTo(0); 46 | try (Channel channel = connection.createChannel(42)) { 47 | assertThat(registry.get("rabbitmq.channels").gauge().value()).isEqualTo(1); 48 | } 49 | assertThat(registry.get("rabbitmq.channels").gauge().value()).isEqualTo(0); 50 | } 51 | } 52 | 53 | @Test 54 | void metrics_collector_is_invoked_on_message_published() throws IOException, TimeoutException { 55 | MockConnectionFactory mockConnectionFactory = new MockConnectionFactory(); 56 | SimpleMeterRegistry registry = new SimpleMeterRegistry(); 57 | mockConnectionFactory.setMetricsCollector(new MicrometerMetricsCollector(registry)); 58 | 59 | try (MockConnection connection = mockConnectionFactory.newConnection(); 60 | Channel channel = connection.createChannel(42)) { 61 | assertThat(registry.get("rabbitmq.published").counter().count()).isEqualTo(0); 62 | channel.basicPublish("", "", null, "".getBytes()); 63 | assertThat(registry.get("rabbitmq.published").counter().count()).isEqualTo(1); 64 | } 65 | } 66 | 67 | @Test 68 | void metrics_collector_is_invoked_on_basic_get_consumption() throws IOException, TimeoutException { 69 | MockConnectionFactory mockConnectionFactory = new MockConnectionFactory(); 70 | SimpleMeterRegistry registry = new SimpleMeterRegistry(); 71 | mockConnectionFactory.setMetricsCollector(new MicrometerMetricsCollector(registry)); 72 | 73 | try (MockConnection connection = mockConnectionFactory.newConnection(); 74 | Channel channel = connection.createChannel(42)) { 75 | String queueName = channel.queueDeclare().getQueue(); 76 | channel.basicPublish("", queueName, null, "".getBytes()); 77 | 78 | assertThat(registry.get("rabbitmq.consumed").counter().count()).isEqualTo(0); 79 | channel.basicGet(queueName, true); 80 | assertThat(registry.get("rabbitmq.consumed").counter().count()).isEqualTo(1); 81 | } 82 | } 83 | 84 | @Test 85 | void metrics_collector_is_invoked_on_basic_ack() throws IOException, TimeoutException { 86 | MockConnectionFactory mockConnectionFactory = new MockConnectionFactory(); 87 | SimpleMeterRegistry registry = new SimpleMeterRegistry(); 88 | mockConnectionFactory.setMetricsCollector(new MicrometerMetricsCollector(registry)); 89 | 90 | try (MockConnection connection = mockConnectionFactory.newConnection(); 91 | Channel channel = connection.createChannel(42)) { 92 | String queueName = channel.queueDeclare().getQueue(); 93 | channel.basicPublish("", queueName, null, "".getBytes()); 94 | GetResponse getResponse = channel.basicGet(queueName, false); 95 | 96 | assertThat(registry.get("rabbitmq.acknowledged").counter().count()).isEqualTo(0); 97 | channel.basicAck(getResponse.getEnvelope().getDeliveryTag(), false); 98 | assertThat(registry.get("rabbitmq.acknowledged").counter().count()).isEqualTo(1); 99 | } 100 | } 101 | 102 | @Test 103 | void metrics_collector_is_invoked_on_basic_nack() throws IOException, TimeoutException { 104 | MockConnectionFactory mockConnectionFactory = new MockConnectionFactory(); 105 | SimpleMeterRegistry registry = new SimpleMeterRegistry(); 106 | mockConnectionFactory.setMetricsCollector(new MicrometerMetricsCollector(registry)); 107 | 108 | try (MockConnection connection = mockConnectionFactory.newConnection(); 109 | Channel channel = connection.createChannel(42)) { 110 | String queueName = channel.queueDeclare().getQueue(); 111 | channel.basicPublish("", queueName, null, "".getBytes()); 112 | GetResponse getResponse = channel.basicGet(queueName, false); 113 | 114 | assertThat(registry.get("rabbitmq.rejected").counter().count()).isEqualTo(0); 115 | channel.basicNack(getResponse.getEnvelope().getDeliveryTag(), false, false); 116 | assertThat(registry.get("rabbitmq.rejected").counter().count()).isEqualTo(1); 117 | } 118 | } 119 | 120 | @Test 121 | void metrics_collector_is_invoked_on_basic_reject() throws IOException, TimeoutException { 122 | MockConnectionFactory mockConnectionFactory = new MockConnectionFactory(); 123 | SimpleMeterRegistry registry = new SimpleMeterRegistry(); 124 | mockConnectionFactory.setMetricsCollector(new MicrometerMetricsCollector(registry)); 125 | 126 | try (MockConnection connection = mockConnectionFactory.newConnection(); 127 | Channel channel = connection.createChannel(42)) { 128 | String queueName = channel.queueDeclare().getQueue(); 129 | channel.basicPublish("", queueName, null, "".getBytes()); 130 | GetResponse getResponse = channel.basicGet(queueName, false); 131 | 132 | assertThat(registry.get("rabbitmq.rejected").counter().count()).isEqualTo(0); 133 | channel.basicReject(getResponse.getEnvelope().getDeliveryTag(), false); 134 | assertThat(registry.get("rabbitmq.rejected").counter().count()).isEqualTo(1); 135 | } 136 | } 137 | 138 | @Test 139 | void metrics_collector_is_invoked_on_consumer_consumption() throws IOException, TimeoutException { 140 | MockConnectionFactory mockConnectionFactory = new MockConnectionFactory(); 141 | SimpleMeterRegistry registry = new SimpleMeterRegistry(); 142 | mockConnectionFactory.setMetricsCollector(new MicrometerMetricsCollector(registry)); 143 | 144 | Supplier publishedMessagesCounter = () -> registry.get("rabbitmq.consumed").counter().count(); 145 | 146 | try (MockConnection connection = mockConnectionFactory.newConnection(); 147 | Channel channel = connection.createChannel(42)) { 148 | String queueName = channel.queueDeclare().getQueue(); 149 | final AtomicBoolean counterIncrementedBeforeHandleDelivery = new AtomicBoolean(); 150 | channel.basicConsume("", new DefaultConsumer(channel) { 151 | @Override 152 | public void handleDelivery(String consumerTag, 153 | Envelope envelope, 154 | AMQP.BasicProperties properties, 155 | byte[] body) { 156 | // Handling the message is not the purpose of this test 157 | counterIncrementedBeforeHandleDelivery.set(publishedMessagesCounter.get() == 1); 158 | } 159 | 160 | @Override 161 | public void handleCancelOk(String consumerTag) { 162 | // Consumer cancellation is not the purpose of this test 163 | } 164 | }); 165 | 166 | assertThat(publishedMessagesCounter.get()).isEqualTo(0); 167 | 168 | channel.basicPublish("", queueName, null, "".getBytes()); 169 | 170 | Assertions.assertTimeoutPreemptively(Duration.ofMillis(200L), () -> { 171 | while (publishedMessagesCounter.get() == 0) { 172 | TimeUnit.MILLISECONDS.sleep(10L); 173 | } 174 | }); 175 | 176 | Awaitility.await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> { 177 | assertThat(publishedMessagesCounter.get()).isEqualTo(1); 178 | assertThat(counterIncrementedBeforeHandleDelivery) 179 | .as("Counter must be incremented before the call to handleDelivery") 180 | .isTrue(); 181 | }); 182 | } 183 | } 184 | 185 | @Test 186 | void metrics_collector_reference_the_last_set_in_connection_factory() throws IOException, TimeoutException { 187 | MockConnectionFactory mockConnectionFactory = new MockConnectionFactory(); 188 | SimpleMeterRegistry registry = new SimpleMeterRegistry(); 189 | 190 | try (MockConnection connection = mockConnectionFactory.newConnection(); 191 | Channel channel = connection.createChannel(42)) { 192 | 193 | mockConnectionFactory.setMetricsCollector(new MicrometerMetricsCollector(registry)); 194 | 195 | String queueName = channel.queueDeclare().getQueue(); 196 | channel.basicPublish("", queueName, null, "".getBytes()); 197 | assertThat(registry.get("rabbitmq.published").counter().count()).isEqualTo(1); 198 | } 199 | } 200 | 201 | @Test 202 | void metrics_recorded_when_single_ack_using_different_channel_to_that_which_declared_queue() throws IOException, TimeoutException { 203 | MockConnectionFactory mockConnectionFactory = new MockConnectionFactory(); 204 | SimpleMeterRegistry registry = new SimpleMeterRegistry(); 205 | mockConnectionFactory.setMetricsCollector(new MicrometerMetricsCollector(registry)); 206 | final AtomicBoolean counterIncrementedBeforeHandleDelivery = new AtomicBoolean(); 207 | 208 | try (MockConnection connection = mockConnectionFactory.newConnection(); 209 | Channel queueCreatingChannel = connection.createChannel(); 210 | Channel queueMutatingChannel = connection.createChannel()) { 211 | 212 | String queueName = queueCreatingChannel.queueDeclare().getQueue(); 213 | 214 | queueMutatingChannel.basicPublish("", queueName, null, "test".getBytes()); 215 | 216 | queueMutatingChannel.basicConsume(queueName, new DefaultConsumer(queueMutatingChannel) { 217 | @Override 218 | public void handleDelivery(String consumerTag, 219 | Envelope envelope, 220 | AMQP.BasicProperties properties, 221 | byte[] body) throws IOException { 222 | counterIncrementedBeforeHandleDelivery.set(true); 223 | queueMutatingChannel.basicAck(envelope.getDeliveryTag(), false); 224 | 225 | } 226 | 227 | @Override 228 | public void handleCancelOk(String consumerTag) { 229 | //Consumer cancellation is not the purpose of this test 230 | } 231 | }); 232 | 233 | Assertions.assertTimeoutPreemptively(Duration.ofMillis(200L), () -> { 234 | while (!counterIncrementedBeforeHandleDelivery.get()) { 235 | TimeUnit.MILLISECONDS.sleep(10L); 236 | } 237 | }); 238 | 239 | Awaitility.await().atMost(1, TimeUnit.SECONDS) 240 | .untilAsserted(() -> 241 | assertThat(registry.get("rabbitmq.acknowledged").counter().count()).isEqualTo(1)); 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/test/java/com/github/fridujo/rabbitmq/mock/MockConnectionFactoryTest.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock; 2 | 3 | import com.github.fridujo.rabbitmq.mock.compatibility.MockConnectionFactoryWithoutAddressResolver; 4 | import com.rabbitmq.client.Address; 5 | import com.rabbitmq.client.Connection; 6 | import com.rabbitmq.client.ConnectionFactory; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.io.IOException; 10 | import java.net.URISyntaxException; 11 | import java.security.KeyManagementException; 12 | import java.security.NoSuchAlgorithmException; 13 | import java.util.List; 14 | import java.util.concurrent.TimeoutException; 15 | 16 | import static org.assertj.core.api.Assertions.assertThat; 17 | 18 | class MockConnectionFactoryTest { 19 | 20 | @Test 21 | void configure_by_params() throws IOException, TimeoutException { 22 | ConnectionFactory factory = new MockConnectionFactory(); 23 | 24 | factory.setUsername("guest"); 25 | factory.setPassword("guest"); 26 | factory.setVirtualHost("/"); 27 | factory.setHost("localhost"); 28 | factory.setPort(5672); 29 | 30 | Connection connection = factory.newConnection(); 31 | 32 | assertThat(connection).isInstanceOf(MockConnection.class); 33 | } 34 | 35 | @Test 36 | void configure_by_uri() throws IOException, TimeoutException, NoSuchAlgorithmException, KeyManagementException, URISyntaxException { 37 | ConnectionFactory factory = new MockConnectionFactory(); 38 | 39 | factory.setUri("amqp://userName:password@hostName:portNumber/virtualHost"); 40 | 41 | Connection connection = factory.newConnection(); 42 | 43 | assertThat(connection).isInstanceOf(MockConnection.class); 44 | } 45 | 46 | @Test 47 | void use_alternate_factory() throws IOException, TimeoutException { 48 | ConnectionFactory factory = new MockConnectionFactoryWithoutAddressResolver(); 49 | 50 | Connection connection = factory.newConnection(null, (List
) null, null); 51 | 52 | assertThat(connection).isInstanceOf(MockConnection.class); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/com/github/fridujo/rabbitmq/mock/MockConnectionTest.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock; 2 | 3 | import com.rabbitmq.client.AMQP; 4 | import com.rabbitmq.client.AlreadyClosedException; 5 | import com.rabbitmq.client.Connection; 6 | import com.rabbitmq.client.ConnectionFactory; 7 | import com.rabbitmq.client.impl.AMQConnection; 8 | import com.rabbitmq.client.impl.DefaultExceptionHandler; 9 | import com.rabbitmq.client.impl.LongStringHelper; 10 | import com.rabbitmq.client.impl.Version; 11 | import org.assertj.core.api.SoftAssertions; 12 | import org.junit.jupiter.api.Test; 13 | 14 | import java.io.IOException; 15 | import java.util.UUID; 16 | 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 19 | 20 | class MockConnectionTest { 21 | 22 | @Test 23 | void connectionParams_are_default_ones() { 24 | Connection connection = new MockConnectionFactory().newConnection(); 25 | 26 | SoftAssertions softly = new SoftAssertions(); 27 | softly.assertThat(connection.getAddress().getHostAddress()).isEqualTo("127.0.0.1"); 28 | softly.assertThat(connection.getPort()).isEqualTo(ConnectionFactory.DEFAULT_AMQP_PORT); 29 | softly.assertThat(connection.getChannelMax()).isEqualTo(0); 30 | softly.assertThat(connection.getFrameMax()).isEqualTo(0); 31 | softly.assertThat(connection.getHeartbeat()).isEqualTo(0); 32 | softly.assertThat(connection.getClientProperties()).isEqualTo(AMQConnection.defaultClientProperties()); 33 | softly.assertThat(connection.getClientProvidedName()).isNull(); 34 | softly.assertThat(connection.getServerProperties().get("version")) 35 | .isEqualTo(LongStringHelper.asLongString(new Version(AMQP.PROTOCOL.MAJOR, AMQP.PROTOCOL.MINOR).toString())); 36 | softly.assertThat(connection.getExceptionHandler()).isExactlyInstanceOf(DefaultExceptionHandler.class); 37 | softly.assertAll(); 38 | } 39 | 40 | @Test 41 | void close_closes_connection() throws IOException { 42 | try (Connection connection = new MockConnectionFactory().newConnection()) { 43 | connection.close(); 44 | assertThat(connection.isOpen()).isFalse(); 45 | } 46 | } 47 | 48 | @Test 49 | void close_with_timeout_closes_connection() throws IOException { 50 | try (Connection connection = new MockConnectionFactory().newConnection()) { 51 | connection.close(10); 52 | assertThat(connection.isOpen()).isFalse(); 53 | } 54 | } 55 | 56 | @Test 57 | void abort_closes_connection() throws IOException { 58 | try (Connection connection = new MockConnectionFactory().newConnection()) { 59 | connection.abort(); 60 | assertThat(connection.isOpen()).isFalse(); 61 | } 62 | } 63 | 64 | @Test 65 | void abort_with_timeout_closes_connection() throws IOException { 66 | try (Connection connection = new MockConnectionFactory().newConnection()) { 67 | connection.abort(15); 68 | assertThat(connection.isOpen()).isFalse(); 69 | } 70 | } 71 | 72 | @Test 73 | void blockedListeners_and_shutdown_listeners_are_not_stored() throws IOException { 74 | try (Connection connection = new MockConnectionFactory().newConnection()) { 75 | assertThat(connection.removeBlockedListener(null)).isTrue(); 76 | assertThat(connection.addBlockedListener(null, null)).isNull(); 77 | connection.clearBlockedListeners(); 78 | connection.addShutdownListener(null); 79 | connection.removeShutdownListener(null); 80 | assertThat(connection.getCloseReason()).isNull(); 81 | } 82 | } 83 | 84 | @Test 85 | void id_is_null_before_being_set() throws IOException { 86 | try (Connection connection = new MockConnectionFactory().newConnection()) { 87 | assertThat(connection.getId()).isNull(); 88 | String id = UUID.randomUUID().toString(); 89 | connection.setId(id); 90 | 91 | assertThat(connection.getId()).isEqualTo(id); 92 | } 93 | } 94 | 95 | @Test 96 | void createChannel_throws_when_connection_is_closed() throws IOException { 97 | try (Connection connection = new MockConnectionFactory().newConnection()) { 98 | connection.close(); 99 | 100 | assertThatExceptionOfType(AlreadyClosedException.class) 101 | .isThrownBy(() -> connection.createChannel()); 102 | } 103 | } 104 | 105 | @Test 106 | void protectedApiMethods_throw() throws IOException { 107 | try (Connection connection = new MockConnectionFactory().newConnection()) { 108 | assertThatExceptionOfType(UnsupportedOperationException.class) 109 | .isThrownBy(() -> connection.notifyListeners()); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/test/java/com/github/fridujo/rabbitmq/mock/RandomStringGeneratorTest.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | class RandomStringGeneratorTest { 8 | 9 | @Test 10 | void nominal_generation() { 11 | RandomStringGenerator randomStringGenerator = new RandomStringGenerator("test-", "AB47", 5); 12 | 13 | assertThat(randomStringGenerator.generate()).matches("test-[AB47]{5}"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/com/github/fridujo/rabbitmq/mock/compatibility/MockConnectionFactoryFactoryTest.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.compatibility; 2 | 3 | import com.github.fridujo.rabbitmq.mock.MockConnectionFactory; 4 | import com.rabbitmq.client.ConnectionFactory; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.net.URL; 8 | import java.net.URLClassLoader; 9 | 10 | import static com.github.fridujo.rabbitmq.mock.tool.SafeArgumentMatchers.eq; 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | import static org.mockito.Mockito.doThrow; 13 | import static org.mockito.Mockito.spy; 14 | 15 | class MockConnectionFactoryFactoryTest { 16 | 17 | @Test 18 | void current_version_resolve_AddressResolver() { 19 | ConnectionFactory connectionFactory = MockConnectionFactoryFactory.build(); 20 | 21 | assertThat(connectionFactory).isExactlyInstanceOf(MockConnectionFactory.class); 22 | } 23 | 24 | @Test 25 | void unresolved_AddressResolver_leads_to_alternate_connectionFactory() throws ClassNotFoundException { 26 | ClassLoader classLoader = spy(new URLClassLoader(new URL[0], this.getClass().getClassLoader())); 27 | doThrow(new ClassNotFoundException()).when(classLoader).loadClass(eq("com.rabbitmq.client.AddressResolver")); 28 | ConnectionFactory connectionFactory = MockConnectionFactoryFactory.build(classLoader); 29 | 30 | assertThat(connectionFactory).isExactlyInstanceOf(MockConnectionFactoryWithoutAddressResolver.class); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/com/github/fridujo/rabbitmq/mock/exchange/ConsistentHashExchangeTests.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.exchange; 2 | 3 | import static com.github.fridujo.rabbitmq.mock.AmqArguments.empty; 4 | import static com.github.fridujo.rabbitmq.mock.exchange.MockExchangeCreator.creatorWithExchangeType; 5 | import static java.util.Collections.emptyMap; 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | import static org.assertj.core.api.Assertions.within; 8 | import static org.mockito.Mockito.mock; 9 | 10 | import java.util.Map; 11 | import java.util.Optional; 12 | import java.util.UUID; 13 | import java.util.function.Function; 14 | import java.util.stream.Collectors; 15 | import java.util.stream.IntStream; 16 | 17 | import org.junit.jupiter.api.Test; 18 | 19 | import com.github.fridujo.rabbitmq.mock.ReceiverPointer; 20 | import com.github.fridujo.rabbitmq.mock.ReceiverRegistry; 21 | import com.github.fridujo.rabbitmq.mock.configuration.Configuration; 22 | 23 | class ConsistentHashExchangeTests { 24 | 25 | private final Configuration configuration = new Configuration() 26 | .registerAdditionalExchangeCreator(creatorWithExchangeType(ConsistentHashExchange.TYPE, ConsistentHashExchange::new)); 27 | private final MockExchangeFactory mockExchangeFactory = new MockExchangeFactory(configuration); 28 | 29 | @Test 30 | void same_routing_key_dispatch_to_same_queue() { 31 | SingleReceiverExchange consistentHashEx = (SingleReceiverExchange) mockExchangeFactory.build("test", "x-consistent-hash", empty(), mock(ReceiverRegistry.class)); 32 | 33 | consistentHashEx.bind(new ReceiverPointer(ReceiverPointer.Type.QUEUE, "Q1"), "1", emptyMap()); 34 | consistentHashEx.bind(new ReceiverPointer(ReceiverPointer.Type.QUEUE, "Q2"), "2", emptyMap()); 35 | 36 | String firstRoutingKey = UUID.randomUUID().toString(); 37 | 38 | ReceiverPointer firstReceiverPointerSelected = consistentHashEx.selectReceiver(firstRoutingKey, null).get(); 39 | 40 | for (; ; ) { 41 | ReceiverPointer receiverPointer = consistentHashEx.selectReceiver(UUID.randomUUID().toString(), null).get(); 42 | if (!receiverPointer.equals(firstReceiverPointerSelected)) { 43 | break; 44 | } 45 | } 46 | 47 | assertThat(consistentHashEx.selectReceiver(firstRoutingKey, null)).contains(firstReceiverPointerSelected); 48 | } 49 | 50 | @Test 51 | void dispatch_respects_queue_weight() { 52 | SingleReceiverExchange consistentHashEx = (SingleReceiverExchange) mockExchangeFactory.build("test", "x-consistent-hash", empty(), mock(ReceiverRegistry.class)); 53 | 54 | ReceiverPointer q1 = new ReceiverPointer(ReceiverPointer.Type.QUEUE, "Q1"); 55 | consistentHashEx.bind(q1, "32", emptyMap()); 56 | ReceiverPointer q2 = new ReceiverPointer(ReceiverPointer.Type.QUEUE, "Q2"); 57 | consistentHashEx.bind(q2, "64", emptyMap()); 58 | ReceiverPointer q3 = new ReceiverPointer(ReceiverPointer.Type.QUEUE, "Q3"); 59 | consistentHashEx.bind(q3, " ", emptyMap()); 60 | ReceiverPointer q4 = new ReceiverPointer(ReceiverPointer.Type.QUEUE, "Q4"); 61 | consistentHashEx.bind(q4, "AA", emptyMap()); 62 | consistentHashEx.unbind(q4, "AA", emptyMap()); 63 | 64 | int messagesCount = 1_000_000; 65 | Map deliveriesByReceiver = IntStream.range(0, messagesCount) 66 | .mapToObj(i -> consistentHashEx.selectReceiver(UUID.randomUUID().toString(), null)) 67 | .map(Optional::get) 68 | .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); 69 | 70 | assertThat(deliveriesByReceiver).containsOnlyKeys(q1, q2, q3); 71 | 72 | assertThat(Long.valueOf(deliveriesByReceiver.get(q1)).doubleValue() / messagesCount).isCloseTo(0.25D, within(0.01)); 73 | assertThat(Long.valueOf(deliveriesByReceiver.get(q2)).doubleValue() / messagesCount).isCloseTo(0.5D, within(0.01)); 74 | assertThat(Long.valueOf(deliveriesByReceiver.get(q3)).doubleValue() / messagesCount).isCloseTo(0.25D, within(0.01)); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/test/java/com/github/fridujo/rabbitmq/mock/exchange/ExchangeTest.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.exchange; 2 | 3 | import static com.github.fridujo.rabbitmq.mock.AmqArguments.empty; 4 | import static com.github.fridujo.rabbitmq.mock.exchange.MockExchangeCreator.creatorWithExchangeType; 5 | import static java.util.Collections.emptyMap; 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 8 | import static org.mockito.ArgumentMatchers.any; 9 | import static org.mockito.Mockito.mock; 10 | import static org.mockito.Mockito.times; 11 | import static org.mockito.Mockito.verify; 12 | import static org.mockito.Mockito.when; 13 | 14 | import java.util.LinkedHashMap; 15 | import java.util.Map; 16 | import java.util.Optional; 17 | 18 | import org.junit.jupiter.api.Nested; 19 | import org.junit.jupiter.api.Test; 20 | import org.junit.jupiter.params.ParameterizedTest; 21 | import org.junit.jupiter.params.provider.CsvSource; 22 | import org.mockito.ArgumentCaptor; 23 | 24 | import com.github.fridujo.rabbitmq.mock.MockNode; 25 | import com.github.fridujo.rabbitmq.mock.MockQueue; 26 | import com.github.fridujo.rabbitmq.mock.ReceiverRegistry; 27 | import com.github.fridujo.rabbitmq.mock.configuration.Configuration; 28 | import com.github.fridujo.rabbitmq.mock.exchange.BindableMockExchange.BindConfiguration; 29 | import com.rabbitmq.client.AMQP; 30 | import com.rabbitmq.client.BuiltinExchangeType; 31 | 32 | class ExchangeTest { 33 | 34 | private final Configuration configuration = new Configuration(); 35 | private final MockExchangeFactory mockExchangeFactory = new MockExchangeFactory(configuration); 36 | 37 | @Test 38 | void mockExchangeFactory_throws_if_type_is_unknown() { 39 | assertThatExceptionOfType(IllegalArgumentException.class) 40 | .isThrownBy(() -> mockExchangeFactory.build("test", "unknown type", empty(), mock(ReceiverRegistry.class))) 41 | .withMessage("No exchange type unknown type"); 42 | } 43 | 44 | @Test 45 | void mockExchangeFactory_register_new_mock_exchange() { 46 | configuration.registerAdditionalExchangeCreator(creatorWithExchangeType(FixDelayExchange.TYPE, FixDelayExchange::new)); 47 | BindableMockExchange xDelayedMockExchange = mockExchangeFactory.build("test", "x-fix-delayed-message", empty(), mock(ReceiverRegistry.class)); 48 | assertThat(xDelayedMockExchange).isExactlyInstanceOf(FixDelayExchange.class); 49 | } 50 | 51 | @Nested 52 | class DirectTest { 53 | @ParameterizedTest(name = "{1} matches {0} as direct bindingKey") 54 | @CsvSource({ 55 | "some.key, some.key", 56 | "some.other.key, some.other.key" 57 | }) 58 | void binding_key_matches_routing_key(String bindingKey, String routingKey) { 59 | MultipleReceiverExchange directExchange = (MultipleReceiverExchange) mockExchangeFactory.build("test", BuiltinExchangeType.DIRECT.getType(), empty(), mock(ReceiverRegistry.class)); 60 | BindConfiguration bindConfiguration = new BindConfiguration(bindingKey, null, emptyMap()); 61 | 62 | assertThat(directExchange.match(bindConfiguration, routingKey, emptyMap())).isTrue(); 63 | } 64 | 65 | @ParameterizedTest(name = "{1} does not match {0} as direct bindingKey") 66 | @CsvSource({ 67 | "some.key, other.key", 68 | "*.orange.*, quick.orange.rabbit", 69 | "lazy.#, lazy.pink.rabbit" 70 | }) 71 | void binding_key_does_not_match_routing_key(String bindingKey, String routingKey) { 72 | MultipleReceiverExchange directExchange = (MultipleReceiverExchange) mockExchangeFactory.build("test", BuiltinExchangeType.DIRECT.getType(), empty(), mock(ReceiverRegistry.class)); 73 | BindConfiguration bindConfiguration = new BindConfiguration(bindingKey, null, emptyMap()); 74 | 75 | assertThat(directExchange.match(bindConfiguration, routingKey, emptyMap())).isFalse(); 76 | } 77 | } 78 | 79 | @Nested 80 | class FanoutTest { 81 | @ParameterizedTest(name = "{1} matches {0} as fanout bindingKey") 82 | @CsvSource({ 83 | "some.key, some.key", 84 | "some.other.key, some.other.key", 85 | "some.key, other.key", 86 | "*.orange.*, quick.orange.rabbit", 87 | "lazy.#, lazy.pink.rabbit", 88 | "some.key, other.key", 89 | "*.orange.*, lazy.pink.rabbit", 90 | "*.orange.*, quick.orange.male.rabbit", 91 | "*.*.rabbit, quick.orange.fox", 92 | "*.*.rabbit, quick.orange.male.rabbit", 93 | "lazy.#, quick.brown.fox" 94 | }) 95 | void binding_key_matches_routing_key(String bindingKey, String routingKey) { 96 | MultipleReceiverExchange fanoutExchange = (MultipleReceiverExchange) mockExchangeFactory.build("test", BuiltinExchangeType.FANOUT.getType(), empty(), mock(ReceiverRegistry.class)); 97 | BindConfiguration bindConfiguration = new BindConfiguration(bindingKey, null, emptyMap()); 98 | 99 | assertThat(fanoutExchange.match(bindConfiguration, routingKey, emptyMap())).isTrue(); 100 | } 101 | } 102 | 103 | @Nested 104 | class TopicTest { 105 | 106 | @ParameterizedTest(name = "{1} matches {0} as topic bindingKey") 107 | @CsvSource({ 108 | "some.key, some.key", 109 | "*.orange.*, quick.orange.rabbit", 110 | "*.*.rabbit, quick.orange.rabbit", 111 | "lazy.#, lazy", 112 | "lazy.#, lazy.pink", 113 | "lazy.#, lazy.pink.rabbit", 114 | "some.#.key.*, some.stuff.key.1", 115 | }) 116 | void binding_key_matches_routing_key(String bindingKey, String routingKey) { 117 | MultipleReceiverExchange topicExchange = (MultipleReceiverExchange) mockExchangeFactory.build("test", BuiltinExchangeType.TOPIC.getType(), empty(), mock(ReceiverRegistry.class)); 118 | BindConfiguration bindConfiguration = new BindConfiguration(bindingKey, null, emptyMap()); 119 | 120 | assertThat(topicExchange.match(bindConfiguration, routingKey, emptyMap())).isTrue(); 121 | } 122 | 123 | @ParameterizedTest(name = "{1} does not match {0} as topic bindingKey") 124 | @CsvSource({ 125 | "some.key, other.key", 126 | "*.orange.*, lazy.pink.rabbit", 127 | "*.orange.*, quick.orange.male.rabbit", 128 | "*.*.rabbit, quick.orange.fox", 129 | "*.*.rabbit, quick.orange.male.rabbit", 130 | "lazy.#, quick.brown.fox", 131 | "lazy.#, lazy1.brown.fox", 132 | "some.#.key.*, some.stuff.key", 133 | "some.#.key.*, some.stuff.key", 134 | "some.#.key.*, some.stuff.key.", 135 | "some.#.key.*, some.stuff.key.one.two", 136 | }) 137 | void binding_key_does_not_match_routing_key(String bindingKey, String routingKey) { 138 | MultipleReceiverExchange topicExchange = (MultipleReceiverExchange) mockExchangeFactory.build("test", BuiltinExchangeType.TOPIC.getType(), empty(), mock(ReceiverRegistry.class)); 139 | BindConfiguration bindConfiguration = new BindConfiguration(bindingKey, null, emptyMap()); 140 | 141 | assertThat(topicExchange.match(bindConfiguration, routingKey, emptyMap())).isFalse(); 142 | } 143 | } 144 | 145 | @Nested 146 | class HeadersTest { 147 | 148 | @ParameterizedTest(name = "[{0}] matching by default headers '{'os: 'linux', cores: '8', brand: null'}': {1}") 149 | @CsvSource({ 150 | "'', false", 151 | "'model, null', false", 152 | "'os, linux', false", 153 | "'os, linux, cores, 4', false", 154 | "'os, linux, cores, 8', false", 155 | "'os, linux, cores, 8, brand, null', true", 156 | "'os, linux, cores, 8, brand, Intel', true" 157 | }) 158 | void headers_topic_without_x_match_does_not_match_if_one_header_is_not_matching(String headers, boolean matches) { 159 | MultipleReceiverExchange headersExchange = (MultipleReceiverExchange) mockExchangeFactory.build("test", BuiltinExchangeType.HEADERS.getType(), empty(), mock(ReceiverRegistry.class)); 160 | BindConfiguration bindConfiguration = new BindConfiguration("unused", null, 161 | map("os", "linux", "cores", "8", "brand", null)); 162 | 163 | assertThat(headersExchange.match(bindConfiguration, "unused", map(headers.split(",\\s*")))).isEqualTo(matches); 164 | } 165 | 166 | @ParameterizedTest(name = "[{0}] matching all headers '{'os: 'linux', cores: '8', brand: null'}': {1}") 167 | @CsvSource({ 168 | "'', false", 169 | "'model, null', false", 170 | "'os, linux', false", 171 | "'os, linux, cores, 4', false", 172 | "'os, linux, cores, 8', false", 173 | "'os, linux, cores, 8, brand, null', true", 174 | "'os, linux, cores, 8, brand, Intel', true" 175 | }) 176 | void headers_topic_with_x_match_all_does_not_match_if_one_header_is_not_matching(String headers, boolean matches) { 177 | MultipleReceiverExchange headersExchange = (MultipleReceiverExchange) mockExchangeFactory.build("test", BuiltinExchangeType.HEADERS.getType(), empty(), mock(ReceiverRegistry.class)); 178 | BindConfiguration bindConfiguration = new BindConfiguration("unused", null, 179 | map("os", "linux", "cores", "8", "brand", null, "x-match", "all")); 180 | 181 | assertThat(headersExchange.match(bindConfiguration, "unused", map(headers.split(",\\s*")))).isEqualTo(matches); 182 | } 183 | 184 | @ParameterizedTest(name = "[{0}] matching any headers '{'os: 'linux', cores: '8', brand: null'}': {1}") 185 | @CsvSource({ 186 | "'', false", 187 | "'model, null', false", 188 | "'os, linux', true", 189 | "'cores, 8', true", 190 | "'os, linux, cores, 4', true", 191 | "'os, linux, cores, 8', true", 192 | "'os, ios, cores, 8', true", 193 | "'brand, null', true", 194 | "'brand, Intel', true", 195 | "'cores, 4, brand, null', true", 196 | "'cores, 4, brand, Intel', true", 197 | "'cores, 8, brand, null', true", 198 | "'cores, 8, brand, Intel', true", 199 | "'os, ios, brand, null', true", 200 | "'os, ios, brand, Intel', true", 201 | "'os, linux, brand, null', true", 202 | "'os, linux, brand, Intel', true" 203 | }) 204 | void headers_topic_with_x_match_any_matches_if_one_header_is_matching(String headers, boolean matches) { 205 | MultipleReceiverExchange headersExchange = (MultipleReceiverExchange) mockExchangeFactory.build("test", BuiltinExchangeType.HEADERS.getType(), empty(), mock(ReceiverRegistry.class)); 206 | BindConfiguration bindConfiguration = new BindConfiguration("unused", null, 207 | map("os", "linux", "cores", "8", "brand", null, "x-match", "any")); 208 | 209 | assertThat(headersExchange.match(bindConfiguration, "unused", map(headers.split(",\\s*")))).isEqualTo(matches); 210 | } 211 | 212 | private Map map(String... keysAndValues) { 213 | Map map = new LinkedHashMap<>(); 214 | String lastKey = null; 215 | for (int i = 0; i < keysAndValues.length; i++) { 216 | if (i % 2 == 0) { 217 | lastKey = keysAndValues[i]; 218 | } else { 219 | map.put(lastKey, "null".equals(keysAndValues[i]) ? null : keysAndValues[i]); 220 | } 221 | } 222 | return map; 223 | } 224 | } 225 | 226 | @Nested 227 | class DefaultExchangeTest { 228 | 229 | @Test 230 | void publish_uses_empty_exchange_name() { 231 | MockQueue mockQueue = mock(MockQueue.class); 232 | MockNode mockNode = mock(MockNode.class); 233 | when(mockNode.getQueue(any())).thenReturn(Optional.of(mockQueue)); 234 | MockDefaultExchange defaultExchange = new MockDefaultExchange(mockNode); 235 | 236 | String previousExchangeName = "ex-previous"; 237 | String routingKey = "rk.test"; 238 | AMQP.BasicProperties props = new AMQP.BasicProperties(); 239 | byte[] body = "test".getBytes(); 240 | 241 | defaultExchange.publish(previousExchangeName, routingKey, props, body); 242 | 243 | ArgumentCaptor publishedExchangeName = ArgumentCaptor.forClass(String.class); 244 | ArgumentCaptor publishedRoutingKey = ArgumentCaptor.forClass(String.class); 245 | ArgumentCaptor publishedProps = ArgumentCaptor.forClass(AMQP.BasicProperties.class); 246 | ArgumentCaptor publishedBody = ArgumentCaptor.forClass(byte[].class); 247 | verify( 248 | mockQueue, 249 | times(1) 250 | ).publish( 251 | publishedExchangeName.capture(), 252 | publishedRoutingKey.capture(), 253 | publishedProps.capture(), 254 | publishedBody.capture() 255 | ); 256 | 257 | assertThat(publishedExchangeName.getValue()).isEmpty(); 258 | assertThat(publishedRoutingKey.getValue()).isSameAs(routingKey); 259 | assertThat(publishedProps.getValue()).isSameAs(props); 260 | assertThat(publishedBody.getValue()).isSameAs(body); 261 | } 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/test/java/com/github/fridujo/rabbitmq/mock/exchange/FixDelayExchange.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.exchange; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | import com.github.fridujo.rabbitmq.mock.AmqArguments; 6 | import com.github.fridujo.rabbitmq.mock.ReceiverRegistry; 7 | import com.rabbitmq.client.AMQP; 8 | 9 | public class FixDelayExchange extends MockDirectExchange { 10 | 11 | public static final String TYPE = "x-fix-delayed-message"; 12 | 13 | public FixDelayExchange(String name, AmqArguments arguments, ReceiverRegistry receiverRegistry) { 14 | super(name, arguments, receiverRegistry); 15 | } 16 | 17 | @Override 18 | public String getType() { 19 | return TYPE; 20 | } 21 | 22 | @Override 23 | public boolean publish(String previousExchangeName, String routingKey, AMQP.BasicProperties props, byte[] body) { 24 | try { 25 | TimeUnit.SECONDS.sleep(1); 26 | } catch (InterruptedException e) { 27 | } 28 | return super.publish(previousExchangeName, routingKey, props, body); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/com/github/fridujo/rabbitmq/mock/metrics/MetricsCollectorWrapperTest.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.metrics; 2 | 3 | import com.github.fridujo.rabbitmq.mock.MockConnectionFactory; 4 | import org.junit.jupiter.api.DynamicTest; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.TestFactory; 7 | 8 | import java.net.URL; 9 | import java.net.URLClassLoader; 10 | import java.util.Arrays; 11 | import java.util.List; 12 | 13 | import static com.github.fridujo.rabbitmq.mock.tool.SafeArgumentMatchers.eq; 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | import static org.junit.jupiter.api.DynamicTest.dynamicTest; 16 | import static org.mockito.Mockito.doThrow; 17 | import static org.mockito.Mockito.spy; 18 | 19 | class MetricsCollectorWrapperTest { 20 | 21 | @Test 22 | void current_version_resolve_MetricsCollector() { 23 | MetricsCollectorWrapper metricsCollectorWrapper = MetricsCollectorWrapper.Builder.build(new MockConnectionFactory()); 24 | 25 | assertThat(metricsCollectorWrapper).isExactlyInstanceOf(ImplementedMetricsCollectorWrapper.class); 26 | } 27 | 28 | @Test 29 | void unresolved_MetricsCollector_leads_to_noop_implementation() throws ClassNotFoundException { 30 | ClassLoader classLoader = spy(new URLClassLoader(new URL[0], this.getClass().getClassLoader())); 31 | doThrow(new ClassNotFoundException()).when(classLoader).loadClass(eq("com.rabbitmq.client.MetricsCollector")); 32 | MetricsCollectorWrapper metricsCollectorWrapper = MetricsCollectorWrapper.Builder.build(classLoader, new MockConnectionFactory()); 33 | 34 | assertThat(metricsCollectorWrapper).isExactlyInstanceOf(NoopMetricsCollectorWrapper.class); 35 | } 36 | 37 | @TestFactory 38 | List noop_implementation_never_throws() { 39 | MetricsCollectorWrapper metricsCollectorWrapper = new NoopMetricsCollectorWrapper(); 40 | return Arrays.asList( 41 | dynamicTest("newConnection", () -> metricsCollectorWrapper.newConnection(null)), 42 | dynamicTest("closeConnection", () -> metricsCollectorWrapper.closeConnection(null)), 43 | dynamicTest("newChannel", () -> metricsCollectorWrapper.newChannel(null)), 44 | dynamicTest("closeChannel", () -> metricsCollectorWrapper.closeChannel(null)), 45 | dynamicTest("basicPublish", () -> metricsCollectorWrapper.basicPublish(null)), 46 | dynamicTest("consumedMessage", () -> metricsCollectorWrapper.consumedMessage(null, 0L, true)), 47 | dynamicTest("consumedMessage (consumerTag)", () -> metricsCollectorWrapper.consumedMessage(null, 0L, null)), 48 | dynamicTest("basicAck", () -> metricsCollectorWrapper.basicAck(null, 0L, true)), 49 | dynamicTest("basicNack", () -> metricsCollectorWrapper.basicNack(null, 0L)), 50 | dynamicTest("basicReject", () -> metricsCollectorWrapper.basicReject(null, 0L)), 51 | dynamicTest("basicConsume", () -> metricsCollectorWrapper.basicConsume(null, null, true)), 52 | dynamicTest("basicCancel", () -> metricsCollectorWrapper.basicCancel(null, null)) 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/com/github/fridujo/rabbitmq/mock/spring/SpringIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.spring; 2 | 3 | import static com.github.fridujo.rabbitmq.mock.exchange.MockExchangeCreator.creatorWithExchangeType; 4 | import static java.time.Duration.ofMillis; 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.UUID; 11 | import java.util.concurrent.ExecutionException; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | import org.junit.jupiter.api.Test; 15 | import org.springframework.amqp.core.BindingBuilder; 16 | import org.springframework.amqp.core.Message; 17 | import org.springframework.amqp.core.Queue; 18 | import org.springframework.amqp.core.TopicExchange; 19 | import org.springframework.amqp.rabbit.AsyncRabbitTemplate; 20 | import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; 21 | import org.springframework.amqp.rabbit.connection.ConnectionFactory; 22 | import org.springframework.amqp.rabbit.core.RabbitAdmin; 23 | import org.springframework.amqp.rabbit.core.RabbitTemplate; 24 | import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; 25 | import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter; 26 | import org.springframework.beans.factory.BeanFactory; 27 | import org.springframework.context.annotation.AnnotationConfigApplicationContext; 28 | import org.springframework.context.annotation.Bean; 29 | import org.springframework.context.annotation.Configuration; 30 | import org.springframework.context.annotation.Import; 31 | 32 | import com.github.fridujo.rabbitmq.mock.compatibility.MockConnectionFactoryFactory; 33 | import com.github.fridujo.rabbitmq.mock.exchange.FixDelayExchange; 34 | 35 | class SpringIntegrationTest { 36 | 37 | private static final String QUEUE_NAME = UUID.randomUUID().toString(); 38 | private static final String EXCHANGE_NAME = UUID.randomUUID().toString(); 39 | 40 | @Test 41 | void basic_get_case() { 42 | String messageBody = "Hello world!"; 43 | try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AmqpConfiguration.class)) { 44 | RabbitTemplate rabbitTemplate = queueAndExchangeSetup(context); 45 | rabbitTemplate.convertAndSend(EXCHANGE_NAME, "test.key1", messageBody); 46 | 47 | Message message = rabbitTemplate.receive(QUEUE_NAME); 48 | 49 | assertThat(message).isNotNull(); 50 | assertThat(message.getBody()).isEqualTo(messageBody.getBytes()); 51 | } 52 | } 53 | 54 | @Test 55 | void basic_consume_case() { 56 | String messageBody = "Hello world!"; 57 | try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AmqpConfiguration.class)) { 58 | RabbitTemplate rabbitTemplate = queueAndExchangeSetup(context); 59 | 60 | Receiver receiver = new Receiver(); 61 | SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); 62 | container.setConnectionFactory(context.getBean(ConnectionFactory.class)); 63 | container.setQueueNames(QUEUE_NAME); 64 | container.setMessageListener(new MessageListenerAdapter(receiver, "receiveMessage")); 65 | try { 66 | container.start(); 67 | 68 | rabbitTemplate.convertAndSend(EXCHANGE_NAME, "test.key2", messageBody); 69 | 70 | List receivedMessages = new ArrayList<>(); 71 | assertTimeoutPreemptively(ofMillis(500L), () -> { 72 | while (receivedMessages.isEmpty()) { 73 | receivedMessages.addAll(receiver.getMessages()); 74 | TimeUnit.MILLISECONDS.sleep(100L); 75 | } 76 | } 77 | ); 78 | 79 | assertThat(receivedMessages).containsExactly(messageBody); 80 | } finally { 81 | container.stop(); 82 | } 83 | } 84 | } 85 | 86 | @Test 87 | void reply_direct_to() throws ExecutionException, InterruptedException { 88 | String messageBody = "Hello world!"; 89 | try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AmqpConfiguration.class)) { 90 | RabbitTemplate rabbitTemplate = queueAndExchangeSetup(context); 91 | 92 | // using AsyncRabbitTemplate to avoid automatic fallback to temporary queue 93 | AsyncRabbitTemplate asyncRabbitTemplate = new AsyncRabbitTemplate(rabbitTemplate); 94 | 95 | Receiver receiver = new Receiver(); 96 | SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); 97 | container.setConnectionFactory(context.getBean(ConnectionFactory.class)); 98 | container.setQueueNames(QUEUE_NAME); 99 | container.setMessageListener(new MessageListenerAdapter(receiver, "receiveMessageAndReply")); 100 | try { 101 | container.start(); 102 | asyncRabbitTemplate.start(); 103 | 104 | var result = asyncRabbitTemplate.convertSendAndReceive(EXCHANGE_NAME, "test.key2", messageBody); 105 | 106 | assertThat(result.get()).isEqualTo(new StringBuilder(messageBody).reverse().toString()); 107 | assertThat(receiver.getMessages()).containsExactly(messageBody); 108 | } finally { 109 | container.stop(); 110 | asyncRabbitTemplate.stop(); 111 | } 112 | } 113 | } 114 | private RabbitTemplate queueAndExchangeSetup(BeanFactory context) { 115 | RabbitAdmin rabbitAdmin = context.getBean(RabbitAdmin.class); 116 | 117 | Queue queue = new Queue(QUEUE_NAME, false); 118 | rabbitAdmin.declareQueue(queue); 119 | TopicExchange exchange = new TopicExchange(EXCHANGE_NAME); 120 | rabbitAdmin.declareExchange(exchange); 121 | rabbitAdmin.declareBinding(BindingBuilder.bind(queue).to(exchange).with("test.*")); 122 | 123 | 124 | return context.getBean(RabbitTemplate.class); 125 | } 126 | 127 | @Configuration 128 | static class AmqpConfiguration { 129 | 130 | @Bean 131 | ConnectionFactory connectionFactory() { 132 | return new CachingConnectionFactory( 133 | MockConnectionFactoryFactory 134 | .build() 135 | .enableConsistentHashPlugin() 136 | .withAdditionalExchange(creatorWithExchangeType("x-fix-delayed-message", FixDelayExchange::new)) 137 | ); 138 | } 139 | 140 | @Bean 141 | RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) { 142 | return new RabbitAdmin(connectionFactory); 143 | } 144 | 145 | @Bean 146 | RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { 147 | return new RabbitTemplate(connectionFactory); 148 | } 149 | } 150 | 151 | @Configuration 152 | @Import(AmqpConfiguration.class) 153 | static class AmqpConsumerConfiguration { 154 | @Bean 155 | SimpleMessageListenerContainer container(ConnectionFactory connectionFactory, 156 | MessageListenerAdapter listenerAdapter) { 157 | SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); 158 | container.setConnectionFactory(connectionFactory); 159 | container.setQueueNames(QUEUE_NAME); 160 | container.setMessageListener(listenerAdapter); 161 | return container; 162 | } 163 | 164 | @Bean 165 | MessageListenerAdapter listenerAdapter(Receiver receiver) { 166 | return new MessageListenerAdapter(receiver, "receiveMessage"); 167 | } 168 | 169 | @Bean 170 | Receiver receiver() { 171 | return new Receiver(); 172 | } 173 | } 174 | 175 | static class Receiver { 176 | private final List messages = new ArrayList<>(); 177 | 178 | public void receiveMessage(String message) { 179 | this.messages.add(message); 180 | } 181 | 182 | public String receiveMessageAndReply(String message) { 183 | this.messages.add(message); 184 | return new StringBuilder(message).reverse().toString(); 185 | } 186 | 187 | List getMessages() { 188 | return messages; 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/test/java/com/github/fridujo/rabbitmq/mock/tool/ClassesTest.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.tool; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | class ClassesTest { 8 | 9 | @Test 10 | void existing_classes_detection() { 11 | assertThat(Classes.missingClass(this.getClass().getClassLoader(), "java.lang.String")).isFalse(); 12 | } 13 | 14 | @Test 15 | void missing_classes_detection() { 16 | assertThat(Classes.missingClass(this.getClass().getClassLoader(), "com.DoesNotExists")).isTrue(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/com/github/fridujo/rabbitmq/mock/tool/ExceptionsTest.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.tool; 2 | 3 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 4 | import static org.mockito.Mockito.doThrow; 5 | import static org.mockito.Mockito.times; 6 | import static org.mockito.Mockito.verify; 7 | 8 | import org.junit.jupiter.api.Test; 9 | import org.mockito.Mockito; 10 | 11 | class ExceptionsTest { 12 | 13 | @Test 14 | void runAndEatExceptions_does_not_throw() throws Exception { 15 | Exceptions.ThrowingRunnable runnable = Mockito.mock(Exceptions.ThrowingRunnable.class); 16 | doThrow(new Exception("test")).when(runnable).run(); 17 | 18 | Exceptions.runAndEatExceptions(runnable); 19 | 20 | verify(runnable, times(1)).run(); 21 | } 22 | 23 | @Test 24 | void runAndTransformExceptions_throws_a_mapped_exception() throws Exception { 25 | Exceptions.ThrowingRunnable runnable = Mockito.mock(Exceptions.ThrowingRunnable.class); 26 | doThrow(new Exception("test")).when(runnable).run(); 27 | 28 | assertThatExceptionOfType(IllegalStateException.class) 29 | .isThrownBy(() -> { 30 | Exceptions.runAndTransformExceptions( 31 | runnable, 32 | e -> new IllegalStateException("transform test", e) 33 | ); 34 | }) 35 | .withMessage("transform test") 36 | .withCauseExactlyInstanceOf(Exception.class); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/com/github/fridujo/rabbitmq/mock/tool/RestartableExecutorServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.tool; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.mockito.ArgumentMatchers.any; 5 | import static org.mockito.ArgumentMatchers.eq; 6 | import static org.mockito.Mockito.atLeastOnce; 7 | import static org.mockito.Mockito.mock; 8 | import static org.mockito.Mockito.verify; 9 | 10 | import java.util.Collections; 11 | import java.util.concurrent.Callable; 12 | import java.util.concurrent.ExecutionException; 13 | import java.util.concurrent.ExecutorService; 14 | import java.util.concurrent.TimeUnit; 15 | import java.util.concurrent.TimeoutException; 16 | import java.util.concurrent.atomic.AtomicInteger; 17 | 18 | import org.junit.jupiter.api.Test; 19 | 20 | class RestartableExecutorServiceTest { 21 | 22 | @Test 23 | void all_calls_delegates() throws InterruptedException, ExecutionException, TimeoutException { 24 | ExecutorService delegate = mock(ExecutorService.class); 25 | ExecutorService executorService = new RestartableExecutorService(() -> delegate); 26 | 27 | executorService.shutdown(); 28 | verify(delegate, atLeastOnce()).shutdown(); 29 | 30 | executorService.shutdownNow(); 31 | verify(delegate, atLeastOnce()).shutdownNow(); 32 | 33 | executorService.isShutdown(); 34 | verify(delegate, atLeastOnce()).isShutdown(); 35 | 36 | executorService.isTerminated(); 37 | verify(delegate, atLeastOnce()).isTerminated(); 38 | 39 | executorService.awaitTermination(3L, TimeUnit.SECONDS); 40 | verify(delegate, atLeastOnce()).awaitTermination(3L, TimeUnit.SECONDS); 41 | 42 | executorService.submit(mock(Callable.class)); 43 | verify(delegate, atLeastOnce()).submit(any(Callable.class)); 44 | 45 | executorService.submit(mock(Runnable.class), new Object()); 46 | verify(delegate, atLeastOnce()).submit(any(Runnable.class), any()); 47 | 48 | executorService.submit(mock(Runnable.class)); 49 | verify(delegate, atLeastOnce()).submit(any(Runnable.class)); 50 | 51 | executorService.invokeAll(Collections.>singletonList(mock(Callable.class))); 52 | verify(delegate, atLeastOnce()).invokeAll(any()); 53 | 54 | executorService.invokeAll(Collections.>singletonList(mock(Callable.class)), 7L, TimeUnit.MILLISECONDS); 55 | verify(delegate, atLeastOnce()).invokeAll(any(), eq(7L), eq(TimeUnit.MILLISECONDS)); 56 | 57 | executorService.invokeAny(Collections.>singletonList(mock(Callable.class))); 58 | verify(delegate, atLeastOnce()).invokeAny(any()); 59 | 60 | executorService.invokeAny(Collections.>singletonList(mock(Callable.class)), 7L, TimeUnit.MILLISECONDS); 61 | verify(delegate, atLeastOnce()).invokeAny(any(), eq(7L), eq(TimeUnit.MILLISECONDS)); 62 | 63 | executorService.execute(mock(Runnable.class)); 64 | verify(delegate, atLeastOnce()).execute(any()); 65 | } 66 | 67 | @Test 68 | void restart_creates_a_new_delegate_from_factory() { 69 | AtomicInteger counter = new AtomicInteger(); 70 | RestartableExecutorService executorService = new RestartableExecutorService(() -> mock(ExecutorService.class, "mock" + counter.incrementAndGet())); 71 | 72 | assertThat(executorService.getDelegate()).hasToString("mock1"); 73 | 74 | executorService.restart(); 75 | 76 | assertThat(executorService.getDelegate()).hasToString("mock2"); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/java/com/github/fridujo/rabbitmq/mock/tool/SafeArgumentMatchers.java: -------------------------------------------------------------------------------- 1 | package com.github.fridujo.rabbitmq.mock.tool; 2 | 3 | import org.mockito.ArgumentMatchers; 4 | 5 | public abstract class SafeArgumentMatchers { 6 | private SafeArgumentMatchers() { 7 | } 8 | 9 | public static String eq(String expected) { 10 | ArgumentMatchers.eq(expected); 11 | return ""; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36}.%L - %msg %ex{4}%nopex%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | --------------------------------------------------------------------------------