├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ └── manual_build.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.en.md ├── README.md ├── allure-notifications-api ├── allure-report │ └── widgets │ │ └── summary.json ├── build.gradle └── src │ ├── main │ ├── java │ │ └── guru │ │ │ └── qa │ │ │ └── allure │ │ │ └── notifications │ │ │ ├── chart │ │ │ ├── Chart.java │ │ │ ├── ChartBuilder.java │ │ │ ├── ChartLegend.java │ │ │ ├── ChartSeries.java │ │ │ └── ChartView.java │ │ │ ├── clients │ │ │ ├── ClientFactory.java │ │ │ ├── Notification.java │ │ │ ├── Notifier.java │ │ │ ├── discord │ │ │ │ └── DiscordClient.java │ │ │ ├── loop │ │ │ │ └── LoopClient.java │ │ │ ├── mail │ │ │ │ ├── Email.java │ │ │ │ ├── Letter.java │ │ │ │ └── LetterBody.java │ │ │ ├── mattermost │ │ │ │ └── MattermostClient.java │ │ │ ├── rocket │ │ │ │ └── RocketChatClient.java │ │ │ ├── skype │ │ │ │ ├── SkypeAuth.java │ │ │ │ ├── SkypeClient.java │ │ │ │ └── model │ │ │ │ │ ├── Attachment.java │ │ │ │ │ ├── From.java │ │ │ │ │ └── SkypeMessage.java │ │ │ ├── slack │ │ │ │ └── SlackClient.java │ │ │ └── telegram │ │ │ │ └── TelegramClient.java │ │ │ ├── config │ │ │ ├── ApplicationConfig.java │ │ │ ├── Config.java │ │ │ ├── base │ │ │ │ └── Base.java │ │ │ ├── discord │ │ │ │ └── Discord.java │ │ │ ├── enums │ │ │ │ └── Language.java │ │ │ ├── loop │ │ │ │ └── Loop.java │ │ │ ├── mail │ │ │ │ ├── Mail.java │ │ │ │ └── SecurityProtocol.java │ │ │ ├── mattermost │ │ │ │ └── Mattermost.java │ │ │ ├── proxy │ │ │ │ └── Proxy.java │ │ │ ├── rocket │ │ │ │ └── RocketChat.java │ │ │ ├── skype │ │ │ │ └── Skype.java │ │ │ ├── slack │ │ │ │ └── Slack.java │ │ │ └── telegram │ │ │ │ └── Telegram.java │ │ │ ├── exceptions │ │ │ ├── InvalidArgumentException.java │ │ │ ├── MessageBuildException.java │ │ │ ├── MessageSendException.java │ │ │ └── MessagingException.java │ │ │ ├── formatters │ │ │ └── Formatters.java │ │ │ ├── json │ │ │ └── JSON.java │ │ │ ├── model │ │ │ ├── legend │ │ │ │ └── Legend.java │ │ │ ├── phrases │ │ │ │ ├── Phrases.java │ │ │ │ └── Scenario.java │ │ │ └── summary │ │ │ │ ├── Statistic.java │ │ │ │ ├── Summary.java │ │ │ │ └── Time.java │ │ │ ├── template │ │ │ ├── MessageTemplate.java │ │ │ └── data │ │ │ │ └── MessageData.java │ │ │ └── util │ │ │ ├── LogInterceptor.java │ │ │ ├── MailProperties.java │ │ │ ├── MailUtil.java │ │ │ ├── ProxyManager.java │ │ │ └── ResourcesUtil.java │ └── resources │ │ ├── legend │ │ ├── by.json │ │ ├── cn.json │ │ ├── cnt.json │ │ ├── en.json │ │ ├── fr.json │ │ ├── ru.json │ │ └── ua.json │ │ ├── phrases │ │ ├── by.json │ │ ├── cn.json │ │ ├── cnt.json │ │ ├── en.json │ │ ├── fr.json │ │ ├── ru.json │ │ └── ua.json │ │ └── templates │ │ ├── html.ftl │ │ ├── markdown.ftl │ │ ├── rocket.ftl │ │ ├── telegram.ftl │ │ └── utils.ftl │ └── test │ ├── java │ └── guru │ │ └── qa │ │ └── allure │ │ └── notifications │ │ ├── clients │ │ ├── NotificationTests.java │ │ ├── mail │ │ │ ├── EmailTests.java │ │ │ └── LetterTests.java │ │ ├── skype │ │ │ └── SkypeClientTest.java │ │ └── telegram │ │ │ └── TelegramClientTest.java │ │ ├── config │ │ └── ApplicationConfigTest.java │ │ ├── formatters │ │ └── FormattersTests.java │ │ ├── template │ │ └── MessageTemplateTests.java │ │ └── util │ │ ├── MailPropertiesTests.java │ │ ├── MailUtilTests.java │ │ └── ProxyManagerTest.java │ └── resources │ ├── data │ ├── testConfig.json │ ├── testConfig_noData.json │ ├── testSuites.json │ └── testSummary.json │ ├── messages │ ├── customHtml.txt │ ├── html.txt │ ├── markdown.txt │ ├── rocket.txt │ └── telegram.txt │ └── template │ ├── emptyTemplate.ftl │ ├── testTemplateAsFile.ftl │ ├── testTemplateAsResource.ftl │ └── utils.ftl ├── allure-notifications ├── build.gradle └── src │ └── main │ ├── java │ └── guru │ │ └── qa │ │ └── allure │ │ └── notifications │ │ └── Application.java │ └── resources │ └── log4j2.xml ├── build.gradle ├── config └── checkstyle │ └── checkstyle.xml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── package.json ├── readme_images ├── email_en.png ├── jenkins_config.png ├── mattermost-ru.png ├── slack-en.png ├── slack_guide │ ├── Screenshot_1.png │ ├── Screenshot_10.png │ ├── Screenshot_11.png │ ├── Screenshot_2.png │ ├── Screenshot_3.png │ ├── Screenshot_4.png │ ├── Screenshot_5.png │ ├── Screenshot_6.png │ ├── Screenshot_7.png │ ├── Screenshot_8.png │ ├── Screenshot_9.png │ └── jenkins_config.png ├── telegram-en.png ├── telegram_ru.png └── telegram_ua.png └── settings.gradle /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gradle" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | java: [8, 11, 17, 21, 24] 18 | fail-fast: false 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Set up JDK 25 | uses: actions/setup-java@v4 26 | with: 27 | java-version: ${{ matrix.java }} 28 | distribution: 'temurin' 29 | 30 | - name: Setup Gradle 31 | uses: gradle/actions/setup-gradle@v4 32 | 33 | - name: Build 34 | run: ./gradlew build 35 | -------------------------------------------------------------------------------- /.github/workflows/manual_build.yml: -------------------------------------------------------------------------------- 1 | name: Publish release of new version manually 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | release_name: 7 | description: 'Name of release (ie bug fixes)' 8 | required: true 9 | tag_name: 10 | description: 'Name of tag (ie testing)' 11 | required: true 12 | 13 | jobs: 14 | build: 15 | name: Publish release 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout changes 20 | uses: actions/checkout@v4 21 | 22 | - name: Set up JDK 1.8 23 | uses: actions/setup-java@v4 24 | with: 25 | distribution: 'temurin' 26 | java-version: 8 27 | 28 | - name: Setup Gradle 29 | uses: gradle/actions/setup-gradle@v4 30 | 31 | - name: Build with Gradle 32 | run: ./gradlew assemble 33 | 34 | - name: Get release version 35 | run: echo "version=$(./gradlew properties -q | grep 'version:' | awk '{print $2}')" >> $GITHUB_ENV 36 | 37 | - name: Print product version 38 | run: echo $version 39 | 40 | - name: Upload jar artifact 41 | uses: actions/upload-artifact@v4 42 | with: 43 | name: 'allure-notifications-${{env.version}}.jar' 44 | path: allure-notifications/build/libs/*.jar 45 | 46 | - name: Create Changelog 47 | id: changelog 48 | uses: scottbrenner/generate-changelog-action@master 49 | env: 50 | REPO: ${{ github.repository }} 51 | 52 | - name: Create Release 53 | id: create_release 54 | uses: actions/create-release@v1 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | with: 58 | tag_name: ${{ github.event.inputs.tag_name }} 59 | release_name: ${{ github.event.inputs.release_name }} 60 | body: ${{ steps.changelog.outputs.changelog }} 61 | draft: false 62 | prerelease: false 63 | 64 | - name: Attach jar to release 65 | id: upload-release-asset 66 | uses: actions/upload-release-asset@v1 67 | env: 68 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 69 | with: 70 | upload_url: ${{ steps.create_release.outputs.upload_url }} 71 | asset_path: allure-notifications/build/libs/allure-notifications-${{env.version}}.jar 72 | asset_name: 'allure-notifications-${{env.version}}.jar' 73 | asset_content_type: application/java-archive 74 | 75 | - name: Publish Release 76 | uses: eregon/publish-release@v1 77 | env: 78 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 79 | with: 80 | release_id: ${{ steps.create_release.outputs.id }} 81 | 82 | - name: Send notification via telegram/slack 83 | uses: appleboy/telegram-action@master 84 | with: 85 | to: ${{ secrets.TELEGRAM_TO }} 86 | token: ${{ secrets.TELEGRAM_TOKEN }} 87 | args: | 88 | *allure-notifications-${{env.version}} is now available for downloading at the link* https://github.com/${{ github.repository }}/releases/latest/download/allure-notifications-${{env.version}}.jar 89 | *See Changelog here*: 90 | https://github.com/${{ github.repository }}/releases/latest/ 91 | format: markdown 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | #*.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | *.iml 22 | 23 | # Project gen files 24 | .idea 25 | .gradle/ 26 | build/ 27 | bin/ 28 | out/ 29 | logs/ 30 | target/ 31 | .classpath 32 | .project 33 | .settings/org.eclipse.buildship.core.prefs 34 | .vscode 35 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v 2.0.1 4 | 5 | ### English 6 | - Updated [README.md](README.md) to use the new command line parsing library 7 | - Added message templates (_ru/en_) 8 | - Added support for English 9 | - Added contract of bots (_AllureBot_) 10 | - Added BaseClient for sending messages (_by default in telegram_) 11 | - Added TelegramClient to encapsulate the logic for sending messages via TelegramBot 12 | - Refactoring of the PieChartBuilder class 13 | - Removed PieChartBot and TextBot 14 | - Added Attachment class, encapsulating photo and text creation for sending via bot 15 | 16 | ### Russian 17 | - Обновлён [README.md](README.md) под использование новой библиотеки парсинга командной строки 18 | - Добавлены темплейты сообщений (_ru/en_) 19 | - Добавлена поддержка английского языка 20 | - Добавлен контракт ботов AllureBot 21 | - Добавлен BaseClient для отправки сообщений (_по умолчанию в telegram_) 22 | - Добавлен TelegramClient для инкапсулирования логики по отправке сообщений через TelegramBot 23 | - Произведён рефакторинг класса PieChartBuilder 24 | - Удалены PieChartBot и TextBot 25 | - Добавлен класс Attachment, инкапсулирующий создание фото и текста для отправки через бота 26 | 27 | ## v 2.0.2 28 | 29 | ### English 30 | 31 | - Fixed bug with passing parameters by keys `-l` and `-e` 32 | - Added Template contract for template development 33 | - Added TemplateData class to store information for reports 34 | - Added RuTemplate and EngTemplate classes that implement the new contract 35 | - Added a Telegram class that implements the generation of a formatted message 36 | - Added a method for generating TemplateData in the Utils class 37 | - TemplateFactory class now returns formatted message 38 | - Implemented workflow for publishing releases with new assemblies to GitHub 39 | 40 | ### Russian 41 | 42 | - Исправлена ошибка с передачей параметров по ключам `-l` и `-e` 43 | - Добавлен контракт Template для разработки шаблонов 44 | - Добавлен класс TemplateData для хранения информации для отчетов 45 | - Добавлены классы RuTemplate и EngTemplate, реализующие новый контракт 46 | - Добавлен класс Telegram, реализующий генерацию отформатированного сообщения 47 | - Добавлен метод по генерации TemplateData в классе Utils 48 | - Класс TemplateFactory теперь возвращает отформатированное сообщение 49 | - Реализован workflow для публикации релизов с новыми сборками в GitHub -------------------------------------------------------------------------------- /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 2021 Qameta Software OÜ 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.en.md: -------------------------------------------------------------------------------- 1 | [![ru](https://img.shields.io/badge/lang-ru-red.svg)](https://github.com/qa-guru/allure-notifications/blob/master/README.md) 2 | 3 | # Allure notifications 4 | **Allure notifications** is the library that allows to send automatic notifications about the results of automated tests to your preferred messenger (Telegram, Slack, Skype, Email, Mattermost, Discord, Loop, Rocket.Chat). 5 | 6 | Notification languages: 🇬🇧 🇫🇷 🇷🇺 🇺🇦 🇧🇾 🇨🇳 7 | 8 | ## Content 9 | + [How it works](#how-it-works) 10 | + [What the notifications look like](#what-the-notifications-look-like) 11 | + [How to use in your project](#how-to-use-in-your-project) 12 | 13 | 14 | ## How it works 15 | After an autotest has finished its work, the `summary.json` file is generated in the `allure-report/widgets` folder. This file contains general statistic about the test results and uses to form the notification sent by the bot (with the diagram and corresponding text). 16 | 17 | Example of a `summary.json` file 18 | ``` 19 | { 20 | "reportName" : "Allure Report", 21 | "testRuns" : [ ], 22 | "statistic" : { 23 | "failed" : 182, 24 | "broken" : 70, 25 | "skipped" : 118, 26 | "passed" : 439, 27 | "unknown" : 42, 28 | "total" : 851 29 | }, 30 | "time" : { 31 | "start" : 1590795193703, 32 | "stop" : 1590932641296, 33 | "duration" : 11311, 34 | "minDuration" : 7901, 35 | "maxDuration" : 109870, 36 | "sumDuration" : 150125 37 | } 38 | } 39 | ``` 40 | In addition, if the Allure Summary plugin is connected, `suites.json` file will also be generated, and data from this file will be included in the statistic. 41 | 42 | ## What the notifications look like 43 | Example of a notification in Telegram 44 | 45 | ![telegram](https://user-images.githubusercontent.com/109241600/213396660-c70adc4c-7a0f-4926-8d9d-473c6c433dd2.png) 46 | 47 | ## How to use in your project 48 | 49 | 1. Setup Java 50 | 2. Create `notifications` folder in the root of your project 51 | 3. Download [the latest version](https://github.com/qa-guru/allure-notifications/releases) of the `allure-notifications-.jar` file and place it in the `notifications` folder in your project 52 | 4. In the `notifications` folder create the `config.json` file with the following structure (keep the base section and the messenger to which notifications need to be sent): 53 | ``` 54 | { 55 | "base": { 56 | "logo": "", 57 | "project": "", 58 | "environment": "", 59 | "comment": "", 60 | "reportLink": "", 61 | "language": "ru", 62 | "allureFolder": "", 63 | "enableChart": false, 64 | "enableSuitesPublishing": false, 65 | "customData": {} 66 | }, 67 | "telegram": { 68 | "token": "", 69 | "chat": "", 70 | "topic": "", 71 | "replyTo": "", 72 | "templatePath": "/templates/telegram.ftl" 73 | }, 74 | "slack": { 75 | "token": "", 76 | "chat": "", 77 | "replyTo": "", 78 | "templatePath": "/templates/markdown.ftl" 79 | }, 80 | "mattermost": { 81 | "url": "", 82 | "token": "", 83 | "chat": "", 84 | "templatePath": "/templates/markdown.ftl" 85 | }, 86 | "rocketChat" : { 87 | "url": "", 88 | "auth_token": "", 89 | "user_id": "", 90 | "channel": "", 91 | "templatePath": "/templates/rocket.ftl" 92 | }, 93 | "skype": { 94 | "appId": "", 95 | "appSecret": "", 96 | "serviceUrl": "", 97 | "conversationId": "", 98 | "botId": "", 99 | "botName": "", 100 | "templatePath": "/templates/markdown.ftl" 101 | }, 102 | "mail": { 103 | "host": "", 104 | "port": "", 105 | "username": "", 106 | "password": "", 107 | "securityProtocol": null, 108 | "from": "", 109 | "to": "", 110 | "cc": "", 111 | "bcc": "", 112 | "templatePath": "/templates/html.ftl" 113 | }, 114 | "discord": { 115 | "botToken": "", 116 | "channelId": "", 117 | "templatePath": "/templates/markdown.ftl" 118 | }, 119 | "loop": { 120 | "webhookUrl": "", 121 | "templatePath": "/templates/markdown.ftl" 122 | }, 123 | "proxy": { 124 | "host": "", 125 | "port": 0, 126 | "username": "", 127 | "password": "" 128 | } 129 | } 130 | ``` 131 | 132 | The `proxy` block is used if you need to specify additional proxy configuration.\ 133 | The `templatePath` parameter is optional and allows to set the path to custom Freemarker template for notification message.\ 134 | Example: 135 | ``` 136 | { 137 | "base": { 138 | ... 139 | }, 140 | "mail": { 141 | "host": "smtp.gmail.com", 142 | "port": "465", 143 | "username": "username", 144 | "password": "password", 145 | "securityProtocol": "SSL", 146 | "from": "test@gmail.com", 147 | "to": "test1@gmail.com", 148 | "cc": "testCC1@gmail.com, testCC2@gmail.com", 149 | "bcc": "testBCC1@gmail.com, testBCC2@gmail.com", 150 | "templatePath": "/templates/html_custom.ftl" 151 | } 152 | } 153 | ``` 154 | 5. Fill the `base` block in the `config.json` file 155 | 156 | Example: 157 | ``` 158 | "base": { 159 | "project": "some project", 160 | "environment": "some env", 161 | "comment": "some comment", 162 | "reportLink": "", 163 | "language": "en", 164 | "allureFolder": "build/allure-report/", 165 | "enableChart": true, 166 | "enableSuitesPublishing": true, 167 | "logo": "logo.png", 168 | "durationFormat": "HH:mm:ss.SSS", 169 | "customData": { 170 | "variable1": "value1", 171 | "variable2": "value2" 172 | } 173 | } 174 | ``` 175 | 176 | Fields: 177 | + `project`, `environment`, `comment` - the name of the project, the name of the environment, and a custom comment. 178 | + `reportLink` - the link to the Allure report with results of tests. 179 | + `language` - the language in which the notification text will be formed (options: `en` / `fr` / `ru` / `ua` / `by` / `cn`). 180 | + `allureFolder` - the path to the folder with Allure results. 181 | + `enableChart` - whether the chart should be displayed (options: `true` / `false`). 182 | + `enableSuitesPublishing` - whether the statistic per suite should be published (options: `true` / `false`, default `false`). Before enabling the option, make sure that the `/widgets` folder contains JSON file `suites.json` 183 | + `logo` - path to the logo file (if filled, the corresponding logo will be displayed in the top left corner of the chart). 184 | + `durationFormat` (optional, default value is `HH:mm:ss.SSS`) - specifies the desired output format for the test duration. 185 | + `customData` - additional data that can be reused in custom Freemarker templates (optional field). 186 | 6. Fill in the `config.json` file block with the information about the chosen messenger. 187 | 7. Execute the following command in terminal: 188 | ``` 189 | java "-DconfigFile=notifications/config.json" -jar notifications/allure-notifications-4.6.1.jar 190 | ``` 191 | Note: 192 | 193 | + The `summary.json` file should already be generated by the time of execution. 194 | + You need to specify the version of the `jar` file that you downloaded in the previous steps in the command-line text. 195 | + Configuration can be overridden via system properties (The system property will take precedence if the same configuration 196 | item is specified in the configuration file). 197 | ```shell 198 | java "-DconfigFile=notifications/config.json" "-Dnotifications.base.environment=${STAND}" "-Dnotifications.base.reportLink=${ALLURE_SERVICE_URL}" "-Dnotifications.base.project=${PROJECT_ID}" "-Dnotifications.telegram.token=${TG_BOT_TOKEN}" "-Dnotifications.telegram.chat=${TG_CHAT_ID}" "-Dnotifications.telegram.topic=${TG_CHAT_TOPIC_ID}" -jar allure-notifications.jar 199 | ``` 200 | 201 | :information_source: The property prefixes for custom data parameters are removed: system property 202 | `-Dbase.customData.variable1=someValue` will result in data with key `variable1` with value `someValue`. 203 | 204 | :warning: `customData` parameter without name is allowed: `base.customData.` 205 | 206 | ## Messenger configurations 207 | +
208 | Telegram config 209 | The `telegram` block parameters: 210 |
    211 |
  • topic - optional parameter defining unique identifier for the target message thread (topic) of 212 | the chat to send the message to; check [Stackoverflow answers](https://stackoverflow.com/questions/74773675/how-to-get-topic-id-for-telegram-group-chat) 213 | to find out how to get the parameter value. 214 |
  • 215 |
216 |
217 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![en](https://img.shields.io/badge/lang-en-red.svg)](https://github.com/qa-guru/allure-notifications/blob/master/README.en.md) 2 | 3 | # Allure notifications 4 | **Allure notifications** - это библиотека, позволяющая выполнять автоматическое оповещение о результатах прохождения автотестов, которое направляется в нужный вам мессенджер (Telegram, Slack, Skype, Email, Mattermost, Discord, Loop, Rocket.Chat). 5 | 6 | Languages: 🇬🇧 🇫🇷 🇷🇺 🇺🇦 🇧🇾 🇨🇳 7 | 8 | ## Содержание 9 | + [Принцип работы](#Принцип) 10 | + [Как выглядят оповещения](#Примеры) 11 | + [Как использовать в своем проекте:](#Настройка) 12 | + [для запуска локально](#Локально) 13 | + [для запуска из Jenkins](#Jenkins) 14 | + [Особенности заполнения файла config.json в зависимости от выбранного мессенджера](#config) 15 | 16 | 17 | 18 | 19 | ## Принцип работы 20 | По итогам выполнения автотестов генерируется файл summary.json в папке allure-report/widgets. 21 | Этот файл содержит общую статистику о результатах прохождения тестов, на основании которой как раз и формируется уведомление, которое отправляет бот (отрисовывается диаграмма и добавляется соответствующий текст). 22 | 23 | image 24 | 25 | 26 | Пример файла summary.json 27 | ``` 28 | { 29 | "reportName" : "Allure Report", 30 | "testRuns" : [ ], 31 | "statistic" : { 32 | "failed" : 182, 33 | "broken" : 70, 34 | "skipped" : 118, 35 | "passed" : 439, 36 | "unknown" : 42, 37 | "total" : 851 38 | }, 39 | "time" : { 40 | "start" : 1590795193703, 41 | "stop" : 1590932641296, 42 | "duration" : 11311, 43 | "minDuration" : 7901, 44 | "maxDuration" : 109870, 45 | "sumDuration" : 150125 46 | } 47 | } 48 | ``` 49 | Кроме этого, если подключен Allure Summary плагин также будет сгенерирован файл `suites.json` данные из которого также будут включены в статистику. 50 | 51 | 52 | 53 | 54 | ## Как выглядят оповещения 55 | Пример оповещения в Telegram 56 | 57 | image 58 | 59 | 60 | 61 | ## Как использовать в своем проекте 62 | 63 | 64 | 65 | 66 | ### Для запуска локально 67 | 1. Для локальной отладки нужно установить java (для запуска в Jenkins она не понадобится) 68 | 2. Создать в корне проекта папку `notifications`. 69 | 3. Скачать актуальную версию файла `allure-notifications-version.jar`, и разместить его в папке `notifications` в своем проекте. 70 | 4. В папке `notifications` создать файл `config.json` со следующей структурой (оставить раздел `base` и тот мессенджер, на который требуется отправлять оповещения): 71 | ``` 72 | { 73 | "base": { 74 | "logo": "", 75 | "project": "", 76 | "environment": "", 77 | "comment": "", 78 | "reportLink": "", 79 | "language": "ru", 80 | "allureFolder": "", 81 | "enableChart": false, 82 | "enableSuitesPublishing": false, 83 | "customData": {} 84 | }, 85 | "telegram": { 86 | "token": "", 87 | "chat": "", 88 | "topic": "", 89 | "replyTo": "", 90 | "templatePath": "/templates/telegram.ftl" 91 | }, 92 | "slack": { 93 | "token": "", 94 | "chat": "", 95 | "replyTo": "", 96 | "templatePath": "/templates/markdown.ftl" 97 | }, 98 | "mattermost": { 99 | "url": "", 100 | "token": "", 101 | "chat": "", 102 | "templatePath": "/templates/markdown.ftl" 103 | }, 104 | "rocketChat" : { 105 | "url": "", 106 | "auth_token": "", 107 | "user_id": "", 108 | "channel": "", 109 | "templatePath": "/templates/rocket.ftl" 110 | }, 111 | "skype": { 112 | "appId": "", 113 | "appSecret": "", 114 | "serviceUrl": "", 115 | "conversationId": "", 116 | "botId": "", 117 | "botName": "", 118 | "templatePath": "/templates/markdown.ftl" 119 | }, 120 | "mail": { 121 | "host": "", 122 | "port": "", 123 | "username": "", 124 | "password": "", 125 | "securityProtocol": null, 126 | "from": "", 127 | "to": "", 128 | "cc": "", 129 | "bcc": "", 130 | "templatePath": "/templates/html.ftl" 131 | }, 132 | "discord": { 133 | "botToken": "", 134 | "channelId": "", 135 | "templatePath": "/templates/markdown.ftl" 136 | }, 137 | "loop": { 138 | "webhookUrl": "", 139 | "templatePath": "/templates/markdown.ftl" 140 | }, 141 | "proxy": { 142 | "host": "", 143 | "port": 0, 144 | "username": "", 145 | "password": "" 146 | } 147 | } 148 | ``` 149 | Блок `proxy` используется если нужно указать дополнительную конфигурацию proxy.\ 150 | Параметр `templatePath` является опциональным и позволяет установить путь к собственному Freemarker шаблону для сообщения. 151 | Пример: 152 | ``` 153 | { 154 | "base": { 155 | ... 156 | }, 157 | "mail": { 158 | "host": "smtp.gmail.com", 159 | "port": "465", 160 | "username": "username", 161 | "password": "password", 162 | "securityProtocol": "SSL", 163 | "from": "test@gmail.com", 164 | "to": "test1@gmail.com", 165 | "cc": "testCC1@gmail.com, testCC2@gmail.com", 166 | "bcc": "testBCC1@gmail.com, testBCC2@gmail.com", 167 | "templatePath": "/templates/html_custom.ftl" 168 | } 169 | } 170 | ``` 171 | 172 | 173 | 174 | 5. Заполнить в файле `config.json` блок `base`: 175 | 176 | Пример заполнения блока `base`: 177 | ``` 178 | "base": { 179 | "project": "some project", 180 | "environment": "some env", 181 | "comment": "some comment", 182 | "reportLink": "", 183 | "language": "en", 184 | "allureFolder": "build/allure-report/", 185 | "enableChart": true, 186 | "enableSuitesPublishing": true, 187 | "logo": "logo.png", 188 | "durationFormat": "HH:mm:ss.SSS", 189 | "customData": { 190 | "variable1": "value1", 191 | "variable2": "value2" 192 | } 193 | } 194 | ``` 195 | Порядок заполнения: 196 | + `project`, `environment`, `comment` - имя проекта, название окружения и произвольный комментарий. 197 | + `reportLink` - ссылка на Allure report с результатами прохождения автотестов (целесообразно заполнять при запуске 198 | автотестов из Jenkins - об этом ниже). 199 | + `language` - язык, на котором будет сформирован текст для оповещения (варианты: en / fr / ru / ua / by / cn). 200 | + `allureFolder` - путь к папке с результатами работы Allure. 201 | + `enableChart` - требуется ли отображать диаграмму (варианты: true / false). 202 | + `enableSuitesPublishing` - требуется ли публиковать отдельно статистику каждого тестового набора (варианты: `true` / `false`, по-умолчанию `false`). Перед включением данной опции убедитесь, что папка `/widgets` содержит JSON файл `suites.json` 203 | + `logo` - путь к файлу с логотипом (если заполнено, то в левом верхнем углу диаграммы будет отображаться соответствующий логотип). 204 | + `durationFormat` (optional, default value is `HH:mm:ss.SSS`) - specifies the desired output format for tests duration. 205 | + `customData` - дополнительные данные, которые могут быть переиспользованы в собственных Freemarker шаблонах (опциональное поле). 206 | 207 | 6. Заполнить в файле `config.json` блок с информацией о выбранном мессенджере: [особенности заполнения файла config.json в зависимости от выбранного мессенджера](#config) 208 | 209 | 7. Выполнить в терминале следующую команду: 210 | ``` 211 | java "-DconfigFile=notifications/config.json" -jar notifications/allure-notifications-4.2.1.jar 212 | ``` 213 | Примечание: 214 | + На момент запуска уже должен быть сформирован файл `summary.json`. 215 | + В тексте команды нужно указать ту версию файла jar, которую вы скачали на предыдущих шагах. 216 | + Настройки можно переопределить через системные переменные (Системная переменная имеет больший приоритет, чем 217 | значение в конфигурационном файле) 218 | ```shell 219 | java "-DconfigFile=notifications/config.json" "-Dnotifications.base.environment=${STAND}" "-Dnotifications.base.reportLink=${ALLURE_SERVICE_URL}" "-Dnotifications.base.project=${PROJECT_ID}" "-Dnotifications.telegram.token=${TG_BOT_TOKEN}" "-Dnotifications.telegram.chat=${TG_CHAT_ID}" "-Dnotifications.telegram.topic=${TG_CHAT_TOPIC_ID}" -jar allure-notifications.jar 220 | ``` 221 | :information_source: Префиксы для дополнительных значений удаляются: 222 | `-Dbase.customData.variable1=someValue` преобразуется в дополнительный параметр `variable1` со значением `someValue` 223 | 224 | :warning: Параметр без указания имени можно использовать : `base.customData.` 225 | 226 | В результате будет сформировано оповещение с результатами прохождения автотестов и направлено в выбранный мессенджер. 227 | 228 | 229 | 230 | 231 | ### Для запуска из Jenkins 232 | 1. Перейти в настройки сборки в Jenkins 233 | 2. В разделе `Сборка` нажать кнопку `Добавить шаг собрки`, в появившемся меню выбрать `Create/Update Text File` 234 | image 235 | 236 | Заполнить следующим образом: 237 | 238 | image 239 | image 240 | 241 | Примечание: 242 | + Общая информация о заполнении блока `base` описана [в этом разделе](#Base) 243 | + В следующих параметрах в качестве значений указываем переменные: `"project": "${JOB_BASE_NAME}"` и `"reportLink": "${BUILD_URL}"`. При формировании уведомления в данных полях будут указаны название `JOB` и ссылка на `BUILD` в Jenkins. 244 | + Особенности заполнения файла config.json в зависимости от выбранного мессенджера описаны [в этом разделе](#config) 245 | 246 | 3. В разделе `Послесборочные операции` нажать кнопку `Добавить шаг после собрки`, в появившемся меню выбрать `Post build task` 247 | image 248 | 249 | + В поле `Script` указываем следующее: 250 | ``` 251 | cd .. 252 | FILE=allure-notifications-4.2.1.jar 253 | if [ ! -f "$FILE" ]; then 254 | wget https://github.com/qa-guru/allure-notifications/releases/download/4.2.1/allure-notifications-4.2.1.jar 255 | fi 256 | ``` 257 | Примечание: 258 | В этом скрипте мы переходим на папку выше, если там нет jar файла, то скачиваем его. Необходимо указать актуальную версию файла jar 259 | 260 | + Нажимаем `Add another task` и во втором поле `Script` указываем следующее: 261 | ``` 262 | java "-DconfigFile=notifications/config.json" -jar ../allure-notifications-4.2.1.jar 263 | ``` 264 | 265 | 4. Сохраняем изменения настроек и запускаем автотесты. По завершении в мессенджер будет направлено уведомление о результатах. 266 | 267 | 268 | 269 | 270 | ## Особенности заполнения файла config.json в зависимости от выбранного мессенджера 271 | + Telegram config 272 | + Параметры блока `telegram`: 273 |
    274 |
  • topic - необязательный параметр, определяющий уникальный идентификатор топика чата, в который 275 | нужно отправить сообщение; посмотрите [ответы на Stackoverflow](https://stackoverflow.com/questions/74773675/how-to-get-topic-id-for-telegram-group-chat), 276 | чтобы узнать, как получить значение параметра. 277 |
  • 278 |
279 | + Slack config 280 | + Email config 281 | + Skype config 282 | + Mattermost config 283 | +
284 | Discord config 285 | To enable Discord notifications it's required to provide 2 configuration parameters: botToken and channelId. 286 |
    287 |
  • To create your own Discord bot and get its token follow these steps. 288 |
      289 |
    1. Turn on “Developer mode” in your Discord account.
    2. 290 |
    3. Click on “Discord API”.
    4. 291 |
    5. In the Developer portal, click on “Applications”. Log in again and then, back in the “Applications” menu, click on “New Application”.
    6. 292 |
    7. Name the bot and then click “Create”.
    8. 293 |
    9. Go to the “Bot” menu and generate a token using “Add Bot”.
    10. 294 |
    11. Copy the bot’s token and paste it into the JSON config
    12. 295 |
    13. Define other details for your bot under “General Information”.
    14. 296 |
    15. Click on “OAuth2”, activate “bot”, set the permissions, and then click on “Copy”.
    16. 297 |
    17. Select your server to add your bot to it.
    18. 298 |
    299 |
  • 300 |
  • To get a Channel ID right click the channel and click on "Copy ID" then paste it into the JSON config. Alternatively type the channel as a mention and place a backslash \ in front of the mention.
  • 301 |
302 |
303 | +
304 | Loop config 305 | To create your own Loop webhook URL follow these steps. 306 |
    307 |
  • Go to main menu of Loop application.
  • 308 |
  • Click "Integrations".
  • 309 |
  • Choose "Incoming Webhooks".
  • 310 |
  • Click "Add Incoming Webhook".
  • 311 |
  • Fill out the form fields on your choice, make sure to select a channel for messages.
  • 312 |
  • Click "Save".
  • 313 |
  • Copy URL of webhook.
  • 314 |
315 |
316 | +
317 | Rocket.Chat config 318 | To enable Rocket.Chat notifications it's required to provide 4 configuration parameters: 319 | url, auth_token,user_id,channel 320 |
    321 |
  • 322 |
      323 |
    1. First of all you need to generate auth_token from user setting.
    2. 324 |
    3. After generation you can get auth_token and user_id.
    4. 325 |
    5. You can get the channel parameter using previously generated tokens and following the documentation.
    6. 326 |
    327 |
  • 328 |
329 |
330 | -------------------------------------------------------------------------------- /allure-notifications-api/allure-report/widgets/summary.json: -------------------------------------------------------------------------------- 1 | { 2 | "reportName" : "Allure Report", 3 | "testRuns" : [ ], 4 | "statistic" : { 5 | "failed" : 182, 6 | "broken" : 70, 7 | "skipped" : 118, 8 | "passed" : 439, 9 | "unknown" : 42, 10 | "total" : 851 11 | }, 12 | "time" : { 13 | "start" : 1590795193703, 14 | "stop" : 1590932641296, 15 | "duration" : 11311, 16 | "minDuration" : 7901, 17 | "maxDuration" : 109870, 18 | "sumDuration" : 150125 19 | } 20 | } -------------------------------------------------------------------------------- /allure-notifications-api/build.gradle: -------------------------------------------------------------------------------- 1 | project.description = 'Library for sending notifications about autotest results (API only)' 2 | 3 | dependencies { 4 | annotationProcessor(group: 'org.projectlombok', name: 'lombok', version: lombokVersion) 5 | compileOnly(group: 'org.projectlombok', name: 'lombok', version: lombokVersion) 6 | 7 | implementation platform(group: 'org.slf4j', name: 'slf4j-bom', version: slf4jVersion) 8 | implementation('org.slf4j:slf4j-api') 9 | 10 | api("com.konghq:unirest-java:${unirestVersion}") 11 | implementation('jakarta.mail:jakarta.mail-api:2.1.3') 12 | runtimeOnly('org.eclipse.angus:smtp:2.0.3') 13 | implementation('org.freemarker:freemarker:2.3.34') 14 | implementation('org.knowm.xchart:xchart:3.8.8') 15 | implementation('com.jayway.jsonpath:json-path:2.9.0') 16 | implementation('org.apache.commons:commons-lang3:3.17.0') 17 | implementation platform('com.fasterxml.jackson:jackson-bom:2.19.0') 18 | implementation('com.fasterxml.jackson.core:jackson-databind') 19 | implementation('com.fasterxml.jackson.dataformat:jackson-dataformat-properties') 20 | 21 | testImplementation('org.junit.jupiter:junit-jupiter:5.13.0') 22 | testRuntimeOnly('org.junit.platform:junit-platform-launcher') 23 | testImplementation('net.bytebuddy:byte-buddy:1.17.5') 24 | testImplementation platform(group: 'org.mockito', name: 'mockito-bom', version: '4.11.0') 25 | testImplementation(group: 'org.mockito', name: 'mockito-junit-jupiter') 26 | testImplementation(group: 'org.mockito', name: 'mockito-inline') 27 | testImplementation "com.konghq:unirest-mocks:${unirestVersion}" 28 | 29 | testImplementation('org.junit-pioneer:junit-pioneer:1.9.1') 30 | testImplementation('net.javacrumbs.json-unit:json-unit:2.39.0') 31 | } 32 | 33 | tasks.withType(Test).configureEach { 34 | useJUnitPlatform() 35 | } 36 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/chart/Chart.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.chart; 2 | 3 | import guru.qa.allure.notifications.exceptions.MessageBuildException; 4 | import guru.qa.allure.notifications.model.legend.Legend; 5 | import guru.qa.allure.notifications.model.summary.Summary; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.knowm.xchart.BitmapEncoder; 8 | import org.knowm.xchart.PieChart; 9 | 10 | import java.awt.Color; 11 | import java.awt.image.BufferedImage; 12 | import java.io.ByteArrayOutputStream; 13 | import java.io.IOException; 14 | import java.util.List; 15 | import javax.imageio.ImageIO; 16 | import java.io.File; 17 | 18 | import guru.qa.allure.notifications.config.base.Base; 19 | 20 | @Slf4j 21 | public class Chart { 22 | 23 | public static byte[] createChart(Base base, Summary summary, Legend legend) throws MessageBuildException { 24 | log.info("Creating chart..."); 25 | PieChart chart = ChartBuilder.createBaseChart(base); 26 | log.info("Adding legend to chart..."); 27 | ChartLegend.addLegendTo(chart); 28 | log.info("Adding view to chart..."); 29 | ChartView.addViewTo(chart); 30 | log.info("Adding series to chart..."); 31 | List colors = new ChartSeries(summary, legend).addSeriesTo(chart); 32 | log.info("Adding colors to series..."); 33 | chart.getStyler().setSeriesColors(colors.toArray(new Color[0])); 34 | BufferedImage chartImage = BitmapEncoder.getBufferedImage(chart); 35 | log.info("Chart is created."); 36 | 37 | if (base.getLogo() != null) { 38 | try { 39 | BufferedImage logo = ImageIO.read(new File(base.getLogo())); 40 | chartImage.getGraphics().drawImage(logo, 3, 3, null); 41 | } catch (Exception e) { 42 | log.warn("Logo file isn't existed: " + base.getLogo()); 43 | } 44 | } 45 | 46 | try { 47 | ByteArrayOutputStream os = new ByteArrayOutputStream(); 48 | ImageIO.write(chartImage, "png", os); 49 | return os.toByteArray(); 50 | } catch (IOException e) { 51 | throw new MessageBuildException("Unable to create image with chart", e); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/chart/ChartBuilder.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.chart; 2 | 3 | import guru.qa.allure.notifications.config.base.Base; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.knowm.xchart.PieChart; 6 | import org.knowm.xchart.PieChartBuilder; 7 | 8 | @Slf4j 9 | public class ChartBuilder { 10 | 11 | public static PieChart createBaseChart(Base base) { 12 | final String title = base.getProject(); 13 | log.info("Creating chart with title {}...", title); 14 | PieChart chart = new PieChartBuilder() 15 | .title(title) 16 | .width(500) 17 | .height(250) 18 | .build(); 19 | log.info("Done."); 20 | return chart; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/chart/ChartLegend.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.chart; 2 | 3 | import org.knowm.xchart.PieChart; 4 | 5 | import static java.awt.Color.WHITE; 6 | import static org.knowm.xchart.style.Styler.LegendLayout.Vertical; 7 | import static org.knowm.xchart.style.Styler.LegendPosition.OutsideE; 8 | 9 | public class ChartLegend { 10 | public static void addLegendTo(PieChart chart) { 11 | chart.getStyler() 12 | .setLegendVisible(true) 13 | .setLegendPosition(OutsideE) 14 | .setLegendPadding(8) 15 | .setLegendBorderColor(WHITE) 16 | .setLegendLayout(Vertical); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/chart/ChartSeries.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.chart; 2 | 3 | import guru.qa.allure.notifications.model.legend.Legend; 4 | import guru.qa.allure.notifications.model.summary.Summary; 5 | import org.knowm.xchart.PieChart; 6 | 7 | import java.awt.*; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | public class ChartSeries { 12 | private final Summary summary; 13 | private final Legend legend; 14 | 15 | public ChartSeries(Summary summary, Legend legend) { 16 | this.summary = summary; 17 | this.legend = legend; 18 | } 19 | 20 | public List addSeriesTo(PieChart chart) { 21 | List colors = new ArrayList<>(); 22 | 23 | addSeries(chart, colors, summary.getStatistic().getPassed(), legend.getPassed(), new Color(148, 202, 102)); 24 | addSeries(chart, colors, summary.getStatistic().getFailed(), legend.getFailed(), new Color(255, 87, 68)); 25 | addSeries(chart, colors, summary.getStatistic().getBroken(), legend.getBroken(), new Color(255, 206, 87)); 26 | addSeries(chart, colors, summary.getStatistic().getSkipped(), legend.getSkipped(), new Color(170, 170, 170)); 27 | addSeries(chart, colors, summary.getStatistic().getUnknown(), legend.getUnknown(), new Color(216, 97, 190)); 28 | 29 | return colors; 30 | } 31 | 32 | private void addSeries(PieChart chart, List colors, Integer value, String legend, Color color) { 33 | if (value != 0) { 34 | chart.addSeries(String.format("%d %s", value, 35 | legend), value); 36 | colors.add(color); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/chart/ChartView.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.chart; 2 | 3 | import org.knowm.xchart.PieChart; 4 | 5 | import static java.awt.Color.WHITE; 6 | import static org.knowm.xchart.PieSeries.PieSeriesRenderStyle.Donut; 7 | 8 | public class ChartView { 9 | public static void addViewTo(PieChart chart) { 10 | chart.getStyler() 11 | .setDefaultSeriesRenderStyle(Donut) 12 | .setCircular(true) 13 | .setSumVisible(true) 14 | .setSumFontSize(30f) 15 | .setDonutThickness(.2); 16 | 17 | chart.getStyler() 18 | .setChartPadding(0) 19 | .setPlotContentSize(.9) 20 | .setPlotBorderColor(WHITE) 21 | .setChartBackgroundColor(WHITE) 22 | .setDecimalPattern("#"); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/clients/ClientFactory.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.clients; 2 | 3 | import guru.qa.allure.notifications.clients.rocket.RocketChatClient; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | import guru.qa.allure.notifications.clients.discord.DiscordClient; 8 | import guru.qa.allure.notifications.clients.loop.LoopClient; 9 | import guru.qa.allure.notifications.clients.mail.Email; 10 | import guru.qa.allure.notifications.clients.mattermost.MattermostClient; 11 | import guru.qa.allure.notifications.clients.skype.SkypeClient; 12 | import guru.qa.allure.notifications.clients.slack.SlackClient; 13 | import guru.qa.allure.notifications.clients.telegram.TelegramClient; 14 | import guru.qa.allure.notifications.config.Config; 15 | 16 | public class ClientFactory { 17 | 18 | public static List from(Config config) { 19 | List notifiers = new ArrayList<>(); 20 | if (config.getTelegram() != null) { 21 | notifiers.add(new TelegramClient(config.getTelegram())); 22 | } 23 | if (config.getSlack() != null) { 24 | notifiers.add(new SlackClient(config.getSlack())); 25 | } 26 | if (config.getMail() != null) { 27 | notifiers.add(new Email(config.getMail())); 28 | } 29 | if (config.getMattermost() != null) { 30 | notifiers.add(new MattermostClient(config.getMattermost())); 31 | } 32 | if (config.getSkype() != null) { 33 | notifiers.add(new SkypeClient(config.getSkype())); 34 | } 35 | if (config.getDiscord() != null) { 36 | notifiers.add(new DiscordClient(config.getDiscord())); 37 | } 38 | if (config.getLoop() != null) { 39 | notifiers.add(new LoopClient(config.getLoop())); 40 | } 41 | if (config.getRocketChat() != null) { 42 | notifiers.add(new RocketChatClient(config.getRocketChat())); 43 | } 44 | return notifiers; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/clients/Notification.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.clients; 2 | 3 | import static java.lang.Boolean.TRUE; 4 | 5 | import guru.qa.allure.notifications.chart.Chart; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.nio.charset.StandardCharsets; 10 | import java.nio.file.Files; 11 | import java.nio.file.Path; 12 | import java.nio.file.Paths; 13 | import java.util.List; 14 | 15 | import guru.qa.allure.notifications.config.Config; 16 | import guru.qa.allure.notifications.config.base.Base; 17 | import guru.qa.allure.notifications.exceptions.MessageBuildException; 18 | import guru.qa.allure.notifications.exceptions.MessagingException; 19 | import guru.qa.allure.notifications.json.JSON; 20 | import guru.qa.allure.notifications.model.legend.Legend; 21 | import guru.qa.allure.notifications.model.phrases.Phrases; 22 | import guru.qa.allure.notifications.model.summary.Summary; 23 | import guru.qa.allure.notifications.template.data.MessageData; 24 | import lombok.extern.slf4j.Slf4j; 25 | 26 | @Slf4j 27 | public class Notification { 28 | private static final String SUITES_DATA_PATH = "widgets/suites.json"; 29 | 30 | public static boolean send(Config config) throws IOException, MessageBuildException { 31 | boolean successfulSending = true; 32 | 33 | final List notifiers = ClientFactory.from(config); 34 | if (notifiers.isEmpty()) { 35 | return successfulSending; 36 | } 37 | 38 | Base base = config.getBase(); 39 | JSON json = new JSON(); 40 | String allureFolderPath = base.getAllureFolder(); 41 | Summary summary = json.parseFile(new File(allureFolderPath, "widgets/summary.json"), Summary.class); 42 | String suitesSummaryJson = null; 43 | Path path = Paths.get(allureFolderPath, SUITES_DATA_PATH); 44 | if (TRUE.equals(base.getEnableSuitesPublishing())) { 45 | if (Files.exists(path)) { 46 | suitesSummaryJson = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); 47 | } else { 48 | log.warn("Suites statistic publishing is enabled, but JSON file with data cannot be found! " 49 | + "Check \"{}\" file in Allure folder.", SUITES_DATA_PATH); 50 | } 51 | } 52 | Phrases phrases = json.parseResource("/phrases/" + base.getLanguage() + ".json", Phrases.class); 53 | MessageData messageData = new MessageData(config.getBase(), summary, suitesSummaryJson, phrases); 54 | byte[] chartImage = null; 55 | if (base.getEnableChart()) { 56 | Legend legend = json.parseResource("/legend/" + base.getLanguage() + ".json", Legend.class); 57 | chartImage = Chart.createChart(base, summary, legend); 58 | } 59 | 60 | for (Notifier notifier : notifiers) { 61 | try { 62 | log.info("Sending message..."); 63 | if (base.getEnableChart()) { 64 | notifier.sendPhoto(messageData, chartImage); 65 | } else { 66 | notifier.sendText(messageData); 67 | } 68 | log.info("Done."); 69 | } catch (MessagingException e) { 70 | successfulSending = false; 71 | log.error(e.getMessage(), e); 72 | } 73 | } 74 | 75 | return successfulSending; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/clients/Notifier.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.clients; 2 | 3 | import guru.qa.allure.notifications.exceptions.MessagingException; 4 | import guru.qa.allure.notifications.template.data.MessageData; 5 | 6 | public interface Notifier { 7 | 8 | void sendText(MessageData messageData) throws MessagingException; 9 | 10 | void sendPhoto(MessageData messageData, byte[] chartImage) throws MessagingException; 11 | } 12 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/clients/discord/DiscordClient.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.clients.discord; 2 | 3 | import guru.qa.allure.notifications.clients.Notifier; 4 | import guru.qa.allure.notifications.config.discord.Discord; 5 | import guru.qa.allure.notifications.exceptions.MessagingException; 6 | import guru.qa.allure.notifications.template.MessageTemplate; 7 | import guru.qa.allure.notifications.template.data.MessageData; 8 | import kong.unirest.ContentType; 9 | import kong.unirest.Unirest; 10 | 11 | import java.io.ByteArrayInputStream; 12 | 13 | public class DiscordClient implements Notifier { 14 | private final Discord discord; 15 | 16 | public DiscordClient(Discord discord) { 17 | this.discord = discord; 18 | } 19 | 20 | @Override 21 | public void sendText(MessageData messageData) throws MessagingException { 22 | Unirest.post("https://discord.com/api/channels/{channelId}/messages") 23 | .routeParam("channelId", discord.getChannelId()) 24 | .header("Authorization", "Bot " + discord.getBotToken()) 25 | .header("Content-Type", ContentType.APPLICATION_FORM_URLENCODED.getMimeType()) 26 | .field("content", MessageTemplate.createMessageFromTemplate(messageData, discord.getTemplatePath())) 27 | .asString() 28 | .getBody(); 29 | } 30 | 31 | @Override 32 | public void sendPhoto(MessageData messageData, byte[] chartImage) throws MessagingException { 33 | Unirest.post("https://discord.com/api/channels/{channelId}/messages") 34 | .routeParam("channelId", discord.getChannelId()) 35 | .header("Authorization", "Bot " + discord.getBotToken()) 36 | .field("file", new ByteArrayInputStream(chartImage), ContentType.IMAGE_PNG, "chart.png") 37 | .field("content", MessageTemplate.createMessageFromTemplate(messageData, discord.getTemplatePath())) 38 | .asString() 39 | .getBody(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/clients/loop/LoopClient.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.clients.loop; 2 | 3 | import guru.qa.allure.notifications.clients.Notifier; 4 | import guru.qa.allure.notifications.config.loop.Loop; 5 | import guru.qa.allure.notifications.exceptions.MessagingException; 6 | import guru.qa.allure.notifications.template.MessageTemplate; 7 | import guru.qa.allure.notifications.template.data.MessageData; 8 | import kong.unirest.ContentType; 9 | import kong.unirest.Unirest; 10 | import java.util.Base64; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | public class LoopClient implements Notifier { 15 | private final Loop loop; 16 | 17 | public LoopClient(Loop loop) { 18 | this.loop = loop; 19 | } 20 | 21 | @Override 22 | public void sendText(MessageData messageData) throws MessagingException { 23 | Map body = new HashMap<>(); 24 | body.put("text", MessageTemplate.createMessageFromTemplate(messageData, loop.getTemplatePath())); 25 | 26 | Unirest.post(loop.getWebhookUrl()) 27 | .header("Content-Type", ContentType.APPLICATION_JSON.getMimeType()) 28 | .body(body) 29 | .asString() 30 | .getBody(); 31 | } 32 | 33 | @Override 34 | public void sendPhoto(MessageData messageData, byte[] chartImage) throws MessagingException { 35 | String encodedChartImage = Base64.getEncoder().encodeToString(chartImage); 36 | 37 | Map body = new HashMap<>(); 38 | body.put("text", MessageTemplate.createMessageFromTemplate(messageData, loop.getTemplatePath())); 39 | 40 | Map attachment = new HashMap<>(); 41 | attachment.put("image_url", "data:image/png;base64," + encodedChartImage); 42 | body.put("attachments", new Object[]{attachment}); 43 | 44 | Unirest.post(loop.getWebhookUrl()) 45 | .header("Content-Type", ContentType.APPLICATION_JSON.getMimeType()) 46 | .body(body) 47 | .asString() 48 | .getBody(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/clients/mail/Email.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.clients.mail; 2 | 3 | import guru.qa.allure.notifications.clients.Notifier; 4 | import guru.qa.allure.notifications.config.mail.Mail; 5 | import guru.qa.allure.notifications.exceptions.MessageBuildException; 6 | import guru.qa.allure.notifications.exceptions.MessagingException; 7 | import guru.qa.allure.notifications.template.MessageTemplate; 8 | import guru.qa.allure.notifications.template.data.MessageData; 9 | import lombok.extern.slf4j.Slf4j; 10 | 11 | @Slf4j 12 | public class Email implements Notifier { 13 | private final Mail mail; 14 | private final Letter letter; 15 | 16 | public Email(Mail mail) { 17 | this.mail = mail; 18 | this.letter = new Letter(mail); 19 | } 20 | 21 | @Override 22 | public void sendText(MessageData messageData) throws MessagingException { 23 | getBaseLetter(messageData) 24 | .text(MessageTemplate.createMessageFromTemplate(messageData, mail.getTemplatePath())) 25 | .send(); 26 | } 27 | 28 | @Override 29 | public void sendPhoto(MessageData messageData, byte[] chartImage) throws MessagingException { 30 | String message = "
" + MessageTemplate.createMessageFromTemplate(messageData, 31 | mail.getTemplatePath()); 32 | getBaseLetter(messageData) 33 | .text(message) 34 | .image(chartImage) 35 | .send(); 36 | } 37 | 38 | private Letter getBaseLetter(MessageData messageData) throws MessageBuildException { 39 | return letter.from(mail.getFrom()) 40 | .to(getTo()) 41 | .cc(mail.getCc()) 42 | .bcc(mail.getBcc()) 43 | .subject(messageData.getProject()); 44 | } 45 | 46 | private String getTo() { 47 | String mailTo = mail.getRecipient(); 48 | if (null != mailTo) { 49 | log.warn("Deprecated \"recipient\" field found in configuration file, use \"to\" field instead."); 50 | if (null != mail.getTo()) { 51 | throw new IllegalArgumentException("Ambiguous configuration fields \"recipient\" and \"to\" found, " 52 | + "use only \"to\" field instead."); 53 | } 54 | } else { 55 | mailTo = mail.getTo(); 56 | } 57 | return mailTo; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/clients/mail/Letter.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.clients.mail; 2 | 3 | import static jakarta.mail.Message.RecipientType; 4 | 5 | import org.apache.commons.lang3.StringUtils; 6 | 7 | import guru.qa.allure.notifications.config.mail.Mail; 8 | import guru.qa.allure.notifications.exceptions.MessageBuildException; 9 | import guru.qa.allure.notifications.exceptions.MessageSendException; 10 | import guru.qa.allure.notifications.util.MailUtil; 11 | import lombok.extern.slf4j.Slf4j; 12 | 13 | import jakarta.mail.Message; 14 | import jakarta.mail.MessagingException; 15 | import jakarta.mail.Transport; 16 | import jakarta.mail.internet.InternetAddress; 17 | import jakarta.mail.internet.MimeMessage; 18 | 19 | @Slf4j 20 | public class Letter { 21 | private final LetterBody body = new LetterBody(); 22 | private final Message letter; 23 | 24 | public Letter(Mail mail) { 25 | letter = new MimeMessage(MailUtil.session(mail)); 26 | } 27 | 28 | public Letter from(final String from) throws MessageBuildException { 29 | log.info("Setting sender..."); 30 | try { 31 | letter.setFrom(new InternetAddress(from)); 32 | } catch (MessagingException e) { 33 | throw new MessageBuildException(String.format("Unable to set sender %s!", from), e); 34 | } 35 | return this; 36 | } 37 | 38 | public Letter to(final String to) throws MessageBuildException { 39 | log.info("Setting recipients..."); 40 | try { 41 | letter.setRecipients( 42 | RecipientType.TO, 43 | MailUtil.recipients(to) 44 | ); 45 | } catch (MessagingException e) { 46 | throw new MessageBuildException(String.format("Unable to set recipients %s!", to), e); 47 | } 48 | log.info("Done."); 49 | return this; 50 | } 51 | 52 | public Letter cc(final String cc) throws MessageBuildException { 53 | return setRecipientsWithTypeIfPresentedInConfig(RecipientType.CC, cc); 54 | } 55 | 56 | public Letter bcc(final String bcc) throws MessageBuildException { 57 | return setRecipientsWithTypeIfPresentedInConfig(RecipientType.BCC, bcc); 58 | } 59 | 60 | private Letter setRecipientsWithTypeIfPresentedInConfig(RecipientType type, String recipients) 61 | throws MessageBuildException { 62 | try { 63 | if (StringUtils.isNotBlank(recipients)) { 64 | letter.setRecipients( 65 | type, 66 | MailUtil.recipients(recipients) 67 | ); 68 | } 69 | } catch (MessagingException e) { 70 | throw new MessageBuildException(String.format("Unable to set recipients %s with type %s!", 71 | recipients, type), e); 72 | } 73 | return this; 74 | } 75 | 76 | public Letter subject(final String subject) throws MessageBuildException { 77 | log.info("Setting subject..."); 78 | try { 79 | letter.setSubject(subject); 80 | } catch (MessagingException e) { 81 | throw new MessageBuildException(String.format("Unable to set subject %s!", subject), e); 82 | } 83 | log.info("Done."); 84 | return this; 85 | } 86 | 87 | public Letter text(final String content) throws MessageBuildException { 88 | log.info("Setting text..."); 89 | body.addText(content); 90 | log.info("Done."); 91 | return this; 92 | } 93 | 94 | public Letter image(final byte[] image) throws MessageBuildException { 95 | log.info("Setting image..."); 96 | body.addImage(image); 97 | log.info("Done."); 98 | return this; 99 | } 100 | 101 | public void send() throws MessageSendException { 102 | log.info("Sending mail..."); 103 | try { 104 | letter.setContent(body.getMultipart()); 105 | Transport.send(letter); 106 | } catch (MessagingException e) { 107 | throw new MessageSendException("Unable to send message!", e); 108 | } 109 | log.info("Done."); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/clients/mail/LetterBody.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.clients.mail; 2 | 3 | import guru.qa.allure.notifications.exceptions.MessageBuildException; 4 | 5 | import jakarta.activation.DataHandler; 6 | import jakarta.activation.DataSource; 7 | import jakarta.mail.BodyPart; 8 | import jakarta.mail.MessagingException; 9 | import jakarta.mail.internet.MimeBodyPart; 10 | import jakarta.mail.internet.MimeMultipart; 11 | import jakarta.mail.util.ByteArrayDataSource; 12 | 13 | public class LetterBody { 14 | private final MimeMultipart multipart = new MimeMultipart("related"); 15 | 16 | public void addText(final String body) throws MessageBuildException { 17 | BodyPart textBody = new MimeBodyPart(); 18 | try { 19 | textBody.setContent(body, "text/html; charset=UTF-8"); 20 | multipart.addBodyPart(textBody); 21 | } catch (MessagingException e) { 22 | throw new MessageBuildException("Unable to create text body!", e); 23 | } 24 | } 25 | 26 | public void addImage(final byte[] image) throws MessageBuildException { 27 | BodyPart imageBody = new MimeBodyPart(); 28 | DataSource dataSource = new ByteArrayDataSource(image, "image/png"); 29 | try { 30 | imageBody.setDataHandler(new DataHandler(dataSource)); 31 | imageBody.setHeader("Content-ID", ""); 32 | multipart.addBodyPart(imageBody); 33 | } catch (MessagingException e) { 34 | throw new MessageBuildException("Unable to create image body!", e); 35 | } 36 | } 37 | 38 | public MimeMultipart getMultipart() { 39 | return multipart; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/clients/mattermost/MattermostClient.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.clients.mattermost; 2 | 3 | import com.jayway.jsonpath.JsonPath; 4 | import guru.qa.allure.notifications.clients.Notifier; 5 | import guru.qa.allure.notifications.config.mattermost.Mattermost; 6 | import guru.qa.allure.notifications.exceptions.MessageBuildException; 7 | import guru.qa.allure.notifications.exceptions.MessagingException; 8 | import guru.qa.allure.notifications.template.MessageTemplate; 9 | import kong.unirest.ContentType; 10 | import guru.qa.allure.notifications.template.data.MessageData; 11 | import kong.unirest.Unirest; 12 | 13 | import java.io.ByteArrayInputStream; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | 17 | import static java.util.Collections.singletonList; 18 | 19 | public class MattermostClient implements Notifier { 20 | private final Mattermost mattermost; 21 | 22 | public MattermostClient(Mattermost mattermost) { 23 | this.mattermost = mattermost; 24 | } 25 | 26 | @Override 27 | public void sendText(MessageData messageData) throws MessagingException { 28 | send(messageData, new HashMap<>()); 29 | } 30 | 31 | @Override 32 | public void sendPhoto(MessageData messageData, byte[] chartImage) throws MessagingException { 33 | String response = Unirest.post("https://{uri}/api/v4/files") 34 | .routeParam("uri", mattermost.getUrl()) 35 | .header("Authorization", "Bearer " + 36 | mattermost.getToken()) 37 | .queryString("channel_id", mattermost.getChat()) 38 | .queryString("filename", "chart") 39 | .field("chart", new ByteArrayInputStream(chartImage), ContentType.IMAGE_PNG, "chart.png") 40 | .asString() 41 | .getBody(); 42 | 43 | String chartId = JsonPath.read(response, "$.file_infos[0].id"); 44 | Map body = new HashMap<>(); 45 | body.put("file_ids", singletonList(chartId)); 46 | send(messageData, body); 47 | } 48 | 49 | private void send(MessageData messageData, Map body) throws MessageBuildException { 50 | body.put("channel_id", mattermost.getChat()); 51 | body.put("message", MessageTemplate.createMessageFromTemplate(messageData, mattermost.getTemplatePath())); 52 | 53 | Unirest.post("https://{uri}/api/v4/posts") 54 | .routeParam("uri", mattermost.getUrl()) 55 | .header("Authorization", "Bearer " + 56 | mattermost.getToken()) 57 | .header("Content-Type", ContentType.APPLICATION_JSON.getMimeType()) 58 | .body(body) 59 | .asString() 60 | .getBody(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/clients/rocket/RocketChatClient.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.clients.rocket; 2 | 3 | import guru.qa.allure.notifications.clients.Notifier; 4 | import guru.qa.allure.notifications.config.rocket.RocketChat; 5 | import guru.qa.allure.notifications.exceptions.MessagingException; 6 | import guru.qa.allure.notifications.template.MessageTemplate; 7 | import guru.qa.allure.notifications.template.data.MessageData; 8 | import java.io.ByteArrayInputStream; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | import kong.unirest.ContentType; 12 | import kong.unirest.Unirest; 13 | 14 | public class RocketChatClient implements Notifier { 15 | private final RocketChat rocketChat; 16 | 17 | public RocketChatClient(RocketChat rocket) { 18 | this.rocketChat = rocket; 19 | } 20 | 21 | @Override 22 | public void sendText(MessageData messageData) throws MessagingException { 23 | Map body = new HashMap<>(); 24 | body.put("channel", rocketChat.getChannel()); 25 | body.put("text", MessageTemplate.createMessageFromTemplate(messageData, rocketChat.getTemplatePath())); 26 | Unirest.post(rocketChat.getUrl() + "/api/v1/chat.postMessage") 27 | .header("X-Auth-Token", rocketChat.getToken()) 28 | .header("X-User-Id", rocketChat.getUserId()) 29 | .header("Content-Type", ContentType.APPLICATION_JSON.getMimeType()) 30 | .body(body) 31 | .asString() 32 | .getBody(); 33 | } 34 | 35 | @Override 36 | public void sendPhoto(MessageData messageData, byte[] chartImage) throws MessagingException { 37 | sendText(messageData); 38 | String url = String.format("%s/api/v1/rooms.upload/%s", rocketChat.getUrl(), rocketChat.getChannel()); 39 | Unirest.post(url) 40 | .header("X-Auth-Token", rocketChat.getToken()) 41 | .header("X-User-Id", rocketChat.getUserId()) 42 | .field("file", new ByteArrayInputStream(chartImage), ContentType.IMAGE_PNG, "chart.png") 43 | .asString() 44 | .getBody(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/clients/skype/SkypeAuth.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.clients.skype; 2 | 3 | import com.jayway.jsonpath.JsonPath; 4 | 5 | import guru.qa.allure.notifications.config.skype.Skype; 6 | import kong.unirest.ContentType; 7 | import kong.unirest.Unirest; 8 | 9 | public class SkypeAuth { 10 | 11 | public static String bearerToken(Skype skype) { 12 | String body = Unirest.post("https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token") 13 | .header("Content-Type", ContentType.APPLICATION_FORM_URLENCODED.getMimeType()) 14 | .field("grant_type", "client_credentials") 15 | .field("client_id", skype.getAppId()) 16 | .field("client_secret", skype.getAppSecret()) 17 | .field("scope", "https://api.botframework.com/.default") 18 | .asString() 19 | .getBody(); 20 | 21 | return JsonPath.read(body, "$.access_token"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/clients/skype/SkypeClient.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.clients.skype; 2 | 3 | import guru.qa.allure.notifications.clients.Notifier; 4 | import guru.qa.allure.notifications.clients.skype.model.Attachment; 5 | import guru.qa.allure.notifications.clients.skype.model.From; 6 | import guru.qa.allure.notifications.clients.skype.model.SkypeMessage; 7 | import guru.qa.allure.notifications.config.skype.Skype; 8 | import guru.qa.allure.notifications.exceptions.MessageBuildException; 9 | import guru.qa.allure.notifications.exceptions.MessagingException; 10 | import guru.qa.allure.notifications.template.MessageTemplate; 11 | import kong.unirest.ContentType; 12 | import guru.qa.allure.notifications.template.data.MessageData; 13 | import kong.unirest.Unirest; 14 | 15 | import java.util.Base64; 16 | import java.util.Collections; 17 | 18 | public class SkypeClient implements Notifier { 19 | private final Skype skype; 20 | 21 | public SkypeClient(Skype skype) { 22 | this.skype = skype; 23 | } 24 | 25 | @Override 26 | public void sendText(MessageData messageData) throws MessagingException { 27 | Unirest.post("https://{url}/apis/v3/conversations/{conversationId}/activities") 28 | .routeParam("url", host()) 29 | .routeParam("conversationId", 30 | skype.getConversationId()) 31 | .header("Content-Type", ContentType.APPLICATION_JSON.getMimeType()) 32 | .header("Authorization", "Bearer " + token()) 33 | .header("Host", host()) 34 | .body(createSimpleMessage(messageData)) 35 | .asString() 36 | .getBody(); 37 | } 38 | 39 | @Override 40 | public void sendPhoto(MessageData messageData, byte[] chartImage) throws MessagingException { 41 | Attachment attachment = Attachment.builder() 42 | .contentType(ContentType.IMAGE_PNG.toString()) 43 | .name("chart.png") 44 | .contentUrl("data:image/png;base64," + Base64.getEncoder().encodeToString(chartImage)) 45 | .build(); 46 | 47 | SkypeMessage body = createSimpleMessage(messageData); 48 | body.setAttachments(Collections.singletonList(attachment)); 49 | 50 | Unirest.post("https://{url}/apis/v3/conversations/{conversationId}/activities") 51 | .routeParam("url", host()) 52 | .routeParam("conversationId", 53 | skype.getConversationId()) 54 | .header("Content-Type", ContentType.APPLICATION_JSON.getMimeType()) 55 | .header("Authorization", "Bearer " + token()) 56 | .header("Host", host()) 57 | .body(body) 58 | .asString() 59 | .getBody(); 60 | } 61 | 62 | private SkypeMessage createSimpleMessage(MessageData messageData) throws MessageBuildException { 63 | From from = From.builder() 64 | .id(skype.getBotId()) 65 | .name(skype.getBotName()) 66 | .build(); 67 | 68 | return SkypeMessage.builder() 69 | .type("message") 70 | .from(from) 71 | .text(MessageTemplate.createMessageFromTemplate(messageData, skype.getTemplatePath())) 72 | .build(); 73 | } 74 | 75 | private String token() { 76 | return SkypeAuth.bearerToken(skype); 77 | } 78 | 79 | private String host() { 80 | return skype.getServiceUrl().substring(0, skype.getServiceUrl().contains("/") 81 | ? skype.getServiceUrl().indexOf("/") : 82 | skype.getServiceUrl().length()); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/clients/skype/model/Attachment.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.clients.skype.model; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | 6 | @Data 7 | @Builder 8 | public class Attachment { 9 | private String contentType; 10 | private String contentUrl; 11 | private String name; 12 | } 13 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/clients/skype/model/From.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.clients.skype.model; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | 6 | @Data 7 | @Builder 8 | public class From { 9 | private String id; 10 | private String name; 11 | } 12 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/clients/skype/model/SkypeMessage.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.clients.skype.model; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | 6 | import java.util.List; 7 | 8 | @Data 9 | @Builder 10 | public class SkypeMessage { 11 | 12 | private String type; 13 | private From from; 14 | private String text; 15 | private List attachments; 16 | } 17 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/clients/slack/SlackClient.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.clients.slack; 2 | 3 | import guru.qa.allure.notifications.clients.Notifier; 4 | import guru.qa.allure.notifications.config.slack.Slack; 5 | import guru.qa.allure.notifications.exceptions.MessageBuildException; 6 | import guru.qa.allure.notifications.exceptions.MessageSendException; 7 | import guru.qa.allure.notifications.exceptions.MessagingException; 8 | import guru.qa.allure.notifications.template.MessageTemplate; 9 | import guru.qa.allure.notifications.template.data.MessageData; 10 | import kong.unirest.json.JSONObject; 11 | import org.apache.http.HttpHeaders; 12 | import org.apache.http.HttpStatus; 13 | import org.apache.http.NameValuePair; 14 | import org.apache.http.client.entity.UrlEncodedFormEntity; 15 | import org.apache.http.client.methods.CloseableHttpResponse; 16 | import org.apache.http.client.methods.HttpUriRequest; 17 | import org.apache.http.client.methods.RequestBuilder; 18 | import org.apache.http.entity.ContentType; 19 | import org.apache.http.entity.mime.HttpMultipartMode; 20 | import org.apache.http.entity.mime.MultipartEntityBuilder; 21 | import org.apache.http.impl.client.CloseableHttpClient; 22 | import org.apache.http.impl.client.HttpClients; 23 | import org.apache.http.message.BasicNameValuePair; 24 | import org.apache.http.util.EntityUtils; 25 | 26 | import java.io.IOException; 27 | import java.nio.charset.StandardCharsets; 28 | import java.util.ArrayList; 29 | import java.util.List; 30 | 31 | public class SlackClient implements Notifier { 32 | private final Slack slack; 33 | 34 | public SlackClient(Slack slack) { 35 | this.slack = slack; 36 | } 37 | 38 | @Override 39 | public void sendText(MessageData messageData) throws MessagingException { 40 | String errorDescription = "Failed to post message to Slack"; 41 | try (CloseableHttpClient client = HttpClients.createDefault()) { 42 | List postMessageFormData = new ArrayList<>(); 43 | postMessageFormData.add(new BasicNameValuePair("channel", slack.getChat())); 44 | postMessageFormData.add(new BasicNameValuePair("text", createMessage(messageData))); 45 | 46 | executeRequest(client, "https://slack.com/api/chat.postMessage", postMessageFormData, errorDescription); 47 | } catch (IOException e) { 48 | throw new MessageSendException(errorDescription, e); 49 | } 50 | } 51 | 52 | @Override 53 | public void sendPhoto(MessageData messageData, byte[] chartImage) throws MessagingException { 54 | try (CloseableHttpClient client = HttpClients.createDefault()) { 55 | HttpUriRequest uploadUrlRequest = RequestBuilder 56 | .get("https://slack.com/api/files.getUploadURLExternal") 57 | .addParameter("filename", "chart.png") 58 | .addParameter("length", String.valueOf(chartImage.length)) 59 | .build(); 60 | JSONObject uploadUrlResponse = new JSONObject( 61 | executeRequest(client, uploadUrlRequest, "Error getting upload URL")); 62 | 63 | String fileId = uploadUrlResponse.getString("file_id"); 64 | String uploadUrl = uploadUrlResponse.getString("upload_url"); 65 | 66 | HttpUriRequest uploadFileRequest = RequestBuilder 67 | .post(uploadUrl) 68 | .setEntity(MultipartEntityBuilder.create() 69 | .setMode(HttpMultipartMode.BROWSER_COMPATIBLE) 70 | .addBinaryBody("file", chartImage, ContentType.DEFAULT_BINARY, "chart") 71 | .build()) 72 | .build(); 73 | executeRequest(client, uploadFileRequest, "Failed to upload file to Slack"); 74 | 75 | List completeUploadFormData = new ArrayList<>(); 76 | completeUploadFormData.add(new BasicNameValuePair("files", "[{\"id\":\"" + fileId + "\"}]")); 77 | completeUploadFormData.add(new BasicNameValuePair("initial_comment", createMessage(messageData))); 78 | completeUploadFormData.add(new BasicNameValuePair("channel_id", slack.getChat())); 79 | 80 | executeRequest(client, "https://slack.com/api/files.completeUploadExternal", completeUploadFormData, 81 | "Error complete upload file"); 82 | } catch (IOException e) { 83 | throw new MessageSendException("Failed to post message with file to Slack", e); 84 | } 85 | } 86 | 87 | private String createMessage(MessageData messageData) throws MessageBuildException { 88 | return MessageTemplate.createMessageFromTemplate(messageData, slack.getTemplatePath()); 89 | } 90 | 91 | private void executeRequest(CloseableHttpClient client, String uri, List formData, 92 | String errorDescription) throws MessageSendException { 93 | HttpUriRequest request = RequestBuilder 94 | .post(uri) 95 | .setEntity(new UrlEncodedFormEntity(formData, StandardCharsets.UTF_8)) 96 | .build(); 97 | executeRequest(client, request, errorDescription); 98 | } 99 | 100 | private String executeRequest(CloseableHttpClient client, HttpUriRequest request, String errorDescription) 101 | throws MessageSendException { 102 | request.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + slack.getToken()); 103 | 104 | try (CloseableHttpResponse responseBody = client.execute(request)) { 105 | int statusCode = responseBody.getStatusLine().getStatusCode(); 106 | String responseAsString = EntityUtils.toString(responseBody.getEntity()); 107 | 108 | if (statusCode == HttpStatus.SC_OK) { 109 | return responseAsString; 110 | } 111 | throw new MessageSendException( 112 | String.format("%s. HTTP status code: %d, HTTP response: %s", errorDescription, statusCode, 113 | responseAsString)); 114 | } catch (IOException e) { 115 | throw new MessageSendException(errorDescription, e); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/clients/telegram/TelegramClient.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.clients.telegram; 2 | 3 | import guru.qa.allure.notifications.clients.Notifier; 4 | import guru.qa.allure.notifications.config.telegram.Telegram; 5 | import guru.qa.allure.notifications.exceptions.MessagingException; 6 | import guru.qa.allure.notifications.template.MessageTemplate; 7 | import guru.qa.allure.notifications.template.data.MessageData; 8 | import kong.unirest.ContentType; 9 | import kong.unirest.MultipartBody; 10 | import kong.unirest.Unirest; 11 | 12 | import java.io.ByteArrayInputStream; 13 | 14 | public class TelegramClient implements Notifier { 15 | private final Telegram telegram; 16 | 17 | public TelegramClient(Telegram telegram) { 18 | this.telegram = telegram; 19 | } 20 | 21 | @Override 22 | public void sendText(MessageData messageData) throws MessagingException { 23 | MultipartBody bodyBuilder = Unirest.post("https://api.telegram.org/bot{token}/sendMessage") 24 | .header("Content-Type", ContentType.APPLICATION_FORM_URLENCODED.getMimeType()) 25 | .field("text", MessageTemplate.createMessageFromTemplate(messageData, telegram.getTemplatePath())); 26 | 27 | configureCommonParameters(bodyBuilder); 28 | 29 | bodyBuilder.asString().getBody(); 30 | } 31 | 32 | @Override 33 | public void sendPhoto(MessageData messageData, byte[] chartImage) throws MessagingException { 34 | MultipartBody bodyBuilder = Unirest.post("https://api.telegram.org/bot{token}/sendPhoto") 35 | .field("photo", new ByteArrayInputStream(chartImage), ContentType.IMAGE_PNG, "chart.png") 36 | .field("caption", MessageTemplate.createMessageFromTemplate(messageData, telegram.getTemplatePath())); 37 | 38 | configureCommonParameters(bodyBuilder); 39 | 40 | bodyBuilder.asString().getBody(); 41 | } 42 | 43 | private void configureCommonParameters(MultipartBody bodyBuilder) { 44 | bodyBuilder 45 | .routeParam("token", telegram.getToken()) 46 | .field("chat_id", telegram.getChat()) 47 | .field("reply_to_message_id", telegram.getReplyTo()) 48 | .field("parse_mode", "HTML"); 49 | 50 | if (this.telegram.getTopic() != null) { 51 | bodyBuilder.field("message_thread_id", this.telegram.getTopic()); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/config/ApplicationConfig.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.config; 2 | 3 | import com.fasterxml.jackson.core.JsonParser; 4 | import com.fasterxml.jackson.databind.DeserializationFeature; 5 | import com.fasterxml.jackson.databind.json.JsonMapper; 6 | import com.fasterxml.jackson.dataformat.javaprop.JavaPropsMapper; 7 | import com.fasterxml.jackson.dataformat.javaprop.JavaPropsSchema; 8 | 9 | import java.io.FileReader; 10 | import java.io.IOException; 11 | import java.util.Map; 12 | 13 | /** 14 | * @author kadehar 15 | * @since 1.0 16 | * Utility class for config creation. 17 | */ 18 | public class ApplicationConfig { 19 | private static final String CONFIG_FILE_PROPERTY_NAME = "configFile"; 20 | 21 | private final String configFile; 22 | 23 | public ApplicationConfig(String configFile) { 24 | this.configFile = configFile; 25 | } 26 | 27 | public ApplicationConfig() { 28 | this(getConfigFile()); 29 | } 30 | 31 | public Config readConfig() throws IOException { 32 | Config config = new JsonMapper().readValue(new FileReader(configFile), Config.class); 33 | mergeWithSystemProperties(config); 34 | return config; 35 | } 36 | 37 | private static void mergeWithSystemProperties(Config config) throws IOException { 38 | JavaPropsMapper javaPropsMapper = JavaPropsMapper.builder() 39 | .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) 40 | .build(); 41 | 42 | try (JsonParser parser = javaPropsMapper.getFactory().createParser((Map) System.getProperties())) { 43 | parser.setSchema(JavaPropsSchema.emptySchema().withPrefix("notifications")); 44 | javaPropsMapper.readerForUpdating(config).readValue(parser, Config.class); 45 | } 46 | } 47 | 48 | private static String getConfigFile() { 49 | String configFile = System.getProperty(CONFIG_FILE_PROPERTY_NAME); 50 | 51 | if (configFile == null || configFile.isEmpty()) { 52 | throw new IllegalArgumentException("'" + CONFIG_FILE_PROPERTY_NAME + "' property is not set or empty: " 53 | + configFile); 54 | } 55 | 56 | return configFile; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/config/Config.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.config; 2 | 3 | import guru.qa.allure.notifications.config.base.Base; 4 | import guru.qa.allure.notifications.config.discord.Discord; 5 | import guru.qa.allure.notifications.config.loop.Loop; 6 | import guru.qa.allure.notifications.config.mail.Mail; 7 | import guru.qa.allure.notifications.config.mattermost.Mattermost; 8 | import guru.qa.allure.notifications.config.proxy.Proxy; 9 | import guru.qa.allure.notifications.config.rocket.RocketChat; 10 | import guru.qa.allure.notifications.config.skype.Skype; 11 | import guru.qa.allure.notifications.config.slack.Slack; 12 | import guru.qa.allure.notifications.config.telegram.Telegram; 13 | import lombok.Data; 14 | 15 | /** 16 | * @author kadehar 17 | * @since 4.0 18 | * Model class representing whole config. 19 | */ 20 | @Data 21 | public class Config { 22 | private Base base; 23 | private Telegram telegram; 24 | private Slack slack; 25 | private Mattermost mattermost; 26 | private Skype skype; 27 | private Mail mail; 28 | private Discord discord; 29 | private Loop loop; 30 | private RocketChat rocketChat; 31 | private Proxy proxy; 32 | } 33 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/config/base/Base.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.config.base; 2 | 3 | import java.util.Map; 4 | 5 | import guru.qa.allure.notifications.config.enums.Language; 6 | import lombok.Data; 7 | 8 | /** 9 | * @author kadehar 10 | * @since 4.0 11 | * Model class representing base settings. 12 | */ 13 | @Data 14 | public class Base { 15 | private String project; 16 | private String environment; 17 | private String comment; 18 | private String reportLink; 19 | private Language language; 20 | private String logo; 21 | private String allureFolder; 22 | private Boolean enableChart; 23 | private Boolean enableSuitesPublishing; 24 | private String durationFormat = "HH:mm:ss.SSS"; 25 | private Map customData; 26 | } 27 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/config/discord/Discord.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.config.discord; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class Discord { 7 | private String botToken; 8 | private String channelId; 9 | private String templatePath = "/templates/markdown.ftl"; 10 | } 11 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/config/enums/Language.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.config.enums; 2 | 3 | public enum Language { 4 | en, fr, ru, ua, cn, cnt, by 5 | } 6 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/config/loop/Loop.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.config.loop; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class Loop { 7 | private String webhookUrl; 8 | private String templatePath = "/templates/markdown.ftl"; 9 | } 10 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/config/mail/Mail.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.config.mail; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @author kadehar 7 | * @since 4.0 8 | * Model class representing mail settings. 9 | */ 10 | @Data 11 | public class Mail { 12 | private String host; 13 | private String port; 14 | private String username; 15 | private String password; 16 | private SecurityProtocol securityProtocol; 17 | private String from; 18 | private String to; 19 | private String cc; 20 | private String bcc; 21 | private String recipient; 22 | private String templatePath = "/templates/html.ftl"; 23 | } 24 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/config/mail/SecurityProtocol.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.config.mail; 2 | 3 | public enum SecurityProtocol { 4 | SSL, 5 | STARTTLS 6 | } 7 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/config/mattermost/Mattermost.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.config.mattermost; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @author kadehar 7 | * @since 4.0 8 | * Model class representing mattermost settings. 9 | */ 10 | @Data 11 | public class Mattermost { 12 | private String url; 13 | private String token; 14 | private String chat; 15 | private String templatePath = "/templates/markdown.ftl"; 16 | } 17 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/config/proxy/Proxy.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.config.proxy; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @author kadehar 7 | * @since 4.0 8 | * Model class representing proxy settings. 9 | */ 10 | @Data 11 | public class Proxy { 12 | private String host; 13 | private Integer port; 14 | private String username; 15 | private String password; 16 | } 17 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/config/rocket/RocketChat.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.config.rocket; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import lombok.Data; 6 | 7 | @Data 8 | public class RocketChat { 9 | private String url; 10 | @JsonProperty("auth_token") 11 | private String token; 12 | @JsonProperty("user_id") 13 | private String userId; 14 | private String channel; 15 | private String templatePath = "/templates/rocket.ftl"; 16 | } 17 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/config/skype/Skype.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.config.skype; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @author kadehar 7 | * @since 4.0 8 | * Model class representing skype settings. 9 | */ 10 | @Data 11 | public class Skype { 12 | private String appId; 13 | private String appSecret; 14 | private String serviceUrl; 15 | private String conversationId; 16 | private String botId; 17 | private String botName; 18 | private String templatePath = "/templates/markdown.ftl"; 19 | } 20 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/config/slack/Slack.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.config.slack; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @author kadehar 7 | * @since 4.0 8 | * Model class representing slack settings. 9 | */ 10 | @Data 11 | public class Slack { 12 | private String token; 13 | private String chat; 14 | private String replyTo; 15 | private String templatePath = "/templates/markdown.ftl"; 16 | } 17 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/config/telegram/Telegram.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.config.telegram; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @author kadehar 7 | * @since 4.0 8 | * Model class representing telegram settings. 9 | */ 10 | @Data 11 | public class Telegram { 12 | private String token; 13 | private String chat; 14 | private String topic; 15 | private String replyTo; 16 | private String templatePath = "/templates/telegram.ftl"; 17 | } 18 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/exceptions/InvalidArgumentException.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.exceptions; 2 | 3 | public class InvalidArgumentException extends RuntimeException { 4 | public InvalidArgumentException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/exceptions/MessageBuildException.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.exceptions; 2 | 3 | public class MessageBuildException extends MessagingException { 4 | public MessageBuildException(String message, Throwable cause) { 5 | super(message, cause); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/exceptions/MessageSendException.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.exceptions; 2 | 3 | public class MessageSendException extends MessagingException { 4 | public MessageSendException(String message, Throwable cause) { 5 | super(message, cause); 6 | } 7 | 8 | public MessageSendException(String message) { 9 | super(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/exceptions/MessagingException.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.exceptions; 2 | 3 | public class MessagingException extends Exception { 4 | public MessagingException(String message, Throwable cause) { 5 | super(message, cause); 6 | } 7 | 8 | public MessagingException(String message) { 9 | super(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/formatters/Formatters.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.formatters; 2 | 3 | import guru.qa.allure.notifications.exceptions.InvalidArgumentException; 4 | import lombok.AccessLevel; 5 | import lombok.NoArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.apache.commons.lang3.time.DurationFormatUtils; 8 | 9 | /** 10 | * @author kadehar 11 | * @since 4.0 12 | * Utility class for data formatting. 13 | */ 14 | @Slf4j 15 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 16 | public class Formatters { 17 | 18 | /** 19 | * Formats the time gap as a string, using the specified format, and padding with zeros. 20 | * 21 | *

This method formats durations using the days and lower fields of the 22 | * format pattern. Months and larger are not used.

23 | * 24 | * @param durationMillis the duration to format 25 | * @param format the way in which to format the duration, not null 26 | * @return the formatted duration, not null 27 | * @throws IllegalArgumentException if durationMillis is null or negative 28 | */ 29 | public static String formatDuration(Long durationMillis, String format) { 30 | if (durationMillis == null) { 31 | throw new InvalidArgumentException("Duration can't be null!"); 32 | } 33 | log.debug("Duration: {} ms", durationMillis); 34 | String formattedDuration = DurationFormatUtils.formatDuration(durationMillis, format); 35 | log.debug("Formatted duration: {}", formattedDuration); 36 | return formattedDuration; 37 | } 38 | 39 | public static String formatReportLink(String link) { 40 | return link != null && link.endsWith("/") ? link + "allure" : link; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/json/JSON.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.json; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import com.google.gson.JsonParser; 6 | import guru.qa.allure.notifications.util.ResourcesUtil; 7 | import lombok.extern.slf4j.Slf4j; 8 | 9 | import java.io.File; 10 | import java.io.FileNotFoundException; 11 | import java.io.FileReader; 12 | import java.io.IOException; 13 | import java.io.InputStream; 14 | import java.io.InputStreamReader; 15 | import java.nio.charset.StandardCharsets; 16 | 17 | /** 18 | * @author kadehar 19 | * @since 4.0 20 | * Utility class for json parsing and pretty printing. 21 | */ 22 | @Slf4j 23 | public class JSON { 24 | private static final ResourcesUtil RESOURCES_UTIL = new ResourcesUtil(); 25 | private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); 26 | 27 | public T parseFile(File file, Class clazz) throws FileNotFoundException { 28 | log.info("Mapping file at path {} to {} object", file.getAbsolutePath(), clazz.getSimpleName()); 29 | return GSON.fromJson(new FileReader(file), clazz); 30 | } 31 | 32 | public T parseResource(String resourcePath, Class clazz) throws IOException { 33 | log.info("Mapping resource at path {} to {} object", resourcePath, clazz.getSimpleName()); 34 | try (InputStream inputStream = RESOURCES_UTIL.getResourceAsStream(resourcePath); 35 | InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) { 36 | return GSON.fromJson(inputStreamReader, clazz); 37 | } 38 | } 39 | 40 | public String prettyPrint(String json) { 41 | return GSON.toJson(JsonParser.parseString(json)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/model/legend/Legend.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.model.legend; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import lombok.Getter; 5 | 6 | /** 7 | * @author kadehar 8 | * @since 4.0 9 | * Model class, representing chart legend phrases. 10 | */ 11 | @Getter 12 | public class Legend { 13 | @SerializedName("passed") 14 | private String passed; 15 | @SerializedName("failed") 16 | private String failed; 17 | @SerializedName("broken") 18 | private String broken; 19 | @SerializedName("unknown") 20 | private String unknown; 21 | @SerializedName("skipped") 22 | private String skipped; 23 | } 24 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/model/phrases/Phrases.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.model.phrases; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import lombok.Getter; 5 | 6 | /** 7 | * @author kadehar 8 | * @since 4.0 9 | * Model class, representing template phrases. 10 | */ 11 | @Getter 12 | public class Phrases { 13 | @SerializedName("results") 14 | private String results; 15 | @SerializedName("environment") 16 | private String environment; 17 | @SerializedName("comment") 18 | private String comment; 19 | @SerializedName("reportAvailableAtLink") 20 | private String reportAvailableAtLink; 21 | @SerializedName("scenario") 22 | private Scenario scenario; 23 | @SerializedName("numberOfSuites") 24 | private String numberOfSuites; 25 | @SerializedName("suiteName") 26 | private String suiteName; 27 | } 28 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/model/phrases/Scenario.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.model.phrases; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import lombok.Getter; 5 | 6 | /** 7 | * @author kadehar 8 | * @since 4.0 9 | * Model class, representing template scenario phrases. 10 | */ 11 | @Getter 12 | public class Scenario { 13 | @SerializedName("duration") 14 | private String duration; 15 | @SerializedName("totalScenarios") 16 | private String totalScenarios; 17 | @SerializedName("totalPassed") 18 | private String totalPassed; 19 | @SerializedName("totalFailed") 20 | private String totalFailed; 21 | @SerializedName("totalBroken") 22 | private String totalBroken; 23 | @SerializedName("totalUnknown") 24 | private String totalUnknown; 25 | @SerializedName("totalSkipped") 26 | private String totalSkipped; 27 | } 28 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/model/summary/Statistic.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.model.summary; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import lombok.Getter; 5 | 6 | /** 7 | * @author kadehar 8 | * @since 1.0 9 | * Model class, representing test statistic from Allure Report. 10 | */ 11 | @Getter 12 | public class Statistic { 13 | @SerializedName("passed") 14 | private Integer passed; 15 | @SerializedName("failed") 16 | private Integer failed; 17 | @SerializedName("broken") 18 | private Integer broken; 19 | @SerializedName("skipped") 20 | private Integer skipped; 21 | @SerializedName("unknown") 22 | private Integer unknown; 23 | @SerializedName("total") 24 | private Integer total; 25 | } 26 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/model/summary/Summary.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.model.summary; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import lombok.Getter; 5 | 6 | /** 7 | * @author kadehar 8 | * @since 1.0 9 | * Model class, representing test summary from Allure Report. 10 | */ 11 | @Getter 12 | public class Summary { 13 | @SerializedName("statistic") 14 | private Statistic statistic; 15 | @SerializedName("time") 16 | private Time time; 17 | } 18 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/model/summary/Time.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.model.summary; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import lombok.Getter; 5 | 6 | /** 7 | * @author kadehar 8 | * @since 1.0 9 | * Model class, representing test duration from Allure Report. 10 | */ 11 | @Getter 12 | public class Time { 13 | @SerializedName("duration") 14 | private Long duration; 15 | } 16 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/template/MessageTemplate.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.template; 2 | 3 | import static freemarker.template.Configuration.VERSION_2_3_31; 4 | 5 | import freemarker.template.Configuration; 6 | import freemarker.template.Template; 7 | import freemarker.template.TemplateException; 8 | import guru.qa.allure.notifications.exceptions.MessageBuildException; 9 | import guru.qa.allure.notifications.template.data.MessageData; 10 | import lombok.extern.slf4j.Slf4j; 11 | 12 | import java.io.File; 13 | import java.io.IOException; 14 | import java.io.StringWriter; 15 | import java.io.Writer; 16 | 17 | /** 18 | * @author kadehar 19 | * @since 4.0 20 | * Utility class for template parsing. 21 | */ 22 | @Slf4j 23 | public class MessageTemplate { 24 | 25 | public static String createMessageFromTemplate(MessageData messageData, String templatePath) 26 | throws MessageBuildException { 27 | try (Writer writer = new StringWriter()) { 28 | log.info("Processing template {}", templatePath); 29 | Template template = getTemplate(templatePath); 30 | log.info("Generating message using template"); 31 | template.process(messageData.getValues(), writer); 32 | return writer.toString(); 33 | } catch (TemplateException | IOException ex) { 34 | throw new MessageBuildException(String.format("Unable to parse template %s!", templatePath), ex); 35 | } 36 | } 37 | 38 | private static Template getTemplate(String templatePath) throws IOException { 39 | final Configuration config = new Configuration(VERSION_2_3_31); 40 | config.setDefaultEncoding("UTF-8"); 41 | 42 | File templateAsFile = new File(templatePath); 43 | if (templateAsFile.exists()) { 44 | config.setDirectoryForTemplateLoading(templateAsFile.getParentFile()); 45 | return config.getTemplate(templateAsFile.getName()); 46 | } else { 47 | config.setClassForTemplateLoading(MessageTemplate.class, "/"); 48 | return config.getTemplate(templatePath); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/template/data/MessageData.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.template.data; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import guru.qa.allure.notifications.config.base.Base; 7 | import guru.qa.allure.notifications.formatters.Formatters; 8 | import guru.qa.allure.notifications.model.phrases.Phrases; 9 | import guru.qa.allure.notifications.model.summary.Summary; 10 | import lombok.extern.slf4j.Slf4j; 11 | 12 | /** 13 | * @author kadehar 14 | * @since 1.0 15 | * Utility class for mapping template data for template. 16 | */ 17 | @Slf4j 18 | public class MessageData { 19 | private final Base base; 20 | private final Summary summary; 21 | private final String suitesSummaryJson; 22 | private final Phrases phrases; 23 | private Map data; 24 | 25 | public MessageData(Base base, Summary summary, String suitesSummaryJson, Phrases phrases) { 26 | this.base = base; 27 | this.summary = summary; 28 | this.suitesSummaryJson = suitesSummaryJson; 29 | this.phrases = phrases; 30 | } 31 | 32 | public String getProject() { 33 | return base.getProject(); 34 | } 35 | 36 | public Map getValues() { 37 | if (data == null) { 38 | this.data = new HashMap<>(); 39 | log.info("Collecting template data"); 40 | 41 | data.put("environment", base.getEnvironment()); 42 | data.put("comment", base.getComment()); 43 | data.put("reportLink", Formatters.formatReportLink(base.getReportLink())); 44 | data.put("customData", base.getCustomData()); 45 | 46 | data.put("time", Formatters.formatDuration(summary.getTime().getDuration(), base.getDurationFormat())); 47 | data.put("statistic", summary.getStatistic()); 48 | 49 | data.put("suitesSummaryJson", suitesSummaryJson); 50 | data.put("phrases", phrases); 51 | log.info("Template data: {}", data); 52 | } 53 | return data; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/util/LogInterceptor.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.util; 2 | 3 | import guru.qa.allure.notifications.json.JSON; 4 | import kong.unirest.*; 5 | import lombok.extern.slf4j.Slf4j; 6 | 7 | @Slf4j 8 | public class LogInterceptor implements Interceptor { 9 | private final JSON json = new JSON(); 10 | 11 | @Override 12 | public void onRequest(HttpRequest request, Config config) { 13 | log.info("\n===REQUEST===\nURL: {}", request.getUrl()); 14 | request.getBody().ifPresent(body -> 15 | logRequestBody(body, request.getHeaders())); 16 | } 17 | 18 | @Override 19 | public void onResponse(HttpResponse response, HttpRequestSummary request, Config config) { 20 | log.info("\n===RESPONSE===\nSTATUS CODE: {}\nBODY: \n{}", 21 | response.getStatus(), 22 | json.prettyPrint(response.getBody().toString())); 23 | } 24 | 25 | private void logRequestBody(Body body, Headers headers) { 26 | if (body.isMultiPart()) { 27 | log.info("BODY: \n{}", body.multiParts()); 28 | return; 29 | } 30 | if (headers.get("Content-Type").contains(ContentType.APPLICATION_FORM_URLENCODED.getMimeType())) { 31 | log.info("BODY: \n{}", body.uniPart()); 32 | return; 33 | } 34 | log.info("BODY: \n{}", json.prettyPrint(body.uniPart().toString())); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/util/MailProperties.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.util; 2 | 3 | import guru.qa.allure.notifications.config.mail.Mail; 4 | import guru.qa.allure.notifications.config.mail.SecurityProtocol; 5 | 6 | import java.util.Locale; 7 | import java.util.Properties; 8 | 9 | public class MailProperties { 10 | private final Mail mail; 11 | 12 | public MailProperties(Mail mail) { 13 | this.mail = mail; 14 | } 15 | 16 | public Properties create() { 17 | Properties properties = new Properties(); 18 | properties.put("mail.smtp.from", mail.getFrom()); 19 | properties.put("mail.smtp.host", mail.getHost()); 20 | properties.put("mail.smtp.port", mail.getPort()); 21 | properties.put("mail.smtp.auth", "true"); 22 | SecurityProtocol securityProtocol = mail.getSecurityProtocol(); 23 | if (securityProtocol != null) { 24 | properties.put("mail.smtp." + securityProtocol.name().toLowerCase(Locale.ROOT) + ".enable", true); 25 | } 26 | return properties; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/util/MailUtil.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.util; 2 | 3 | import guru.qa.allure.notifications.config.mail.Mail; 4 | import guru.qa.allure.notifications.exceptions.InvalidArgumentException; 5 | import lombok.extern.slf4j.Slf4j; 6 | 7 | import jakarta.mail.Authenticator; 8 | import jakarta.mail.PasswordAuthentication; 9 | import jakarta.mail.Session; 10 | import jakarta.mail.internet.AddressException; 11 | import jakarta.mail.internet.InternetAddress; 12 | import java.util.ArrayList; 13 | import java.util.Arrays; 14 | import java.util.List; 15 | import java.util.Properties; 16 | 17 | @Slf4j 18 | public class MailUtil { 19 | 20 | public static Session session(Mail mail) { 21 | log.info("Creating new session"); 22 | Properties properties = new MailProperties(mail).create(); 23 | return Session.getDefaultInstance( 24 | properties, 25 | new Authenticator() { 26 | @Override 27 | protected PasswordAuthentication getPasswordAuthentication() { 28 | return new PasswordAuthentication(mail.getUsername(), 29 | mail.getPassword()); 30 | } 31 | } 32 | ); 33 | } 34 | 35 | public static InternetAddress[] recipients(String addresses) { 36 | log.info("Parsing addresses"); 37 | List addressList = new ArrayList<>(); 38 | 39 | if (addresses == null || addresses.isEmpty()) { 40 | throw new InvalidArgumentException("Recipients can't be null!"); 41 | } 42 | 43 | String[] addressesArray = addresses.split(",\\h*"); 44 | for (String address : addressesArray) { 45 | try { 46 | addressList.add(new InternetAddress(address)); 47 | } catch (AddressException e) { 48 | throw new InvalidArgumentException("Invalid email address: " + address); 49 | } 50 | } 51 | 52 | InternetAddress[] recipients = addressList.toArray(new InternetAddress[0]); 53 | log.info("Recipients: {}", Arrays.toString(recipients)); 54 | return recipients; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/util/ProxyManager.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.util; 2 | 3 | import guru.qa.allure.notifications.config.proxy.Proxy; 4 | import kong.unirest.Unirest; 5 | import org.apache.commons.lang3.StringUtils; 6 | 7 | public class ProxyManager { 8 | public static void manageProxy(Proxy proxy) { 9 | if (proxy != null) { 10 | if (StringUtils.isNotEmpty(proxy.getHost()) && proxy.getPort() != 0) { 11 | if (StringUtils.isNotEmpty(proxy.getUsername()) && StringUtils.isNotEmpty(proxy.getPassword())) { 12 | Unirest.config().proxy(proxy.getHost(), proxy.getPort(), 13 | proxy.getUsername(), proxy.getPassword()); 14 | } else { 15 | Unirest.config().proxy(proxy.getHost(), proxy.getPort()); 16 | } 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/java/guru/qa/allure/notifications/util/ResourcesUtil.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.util; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.nio.file.Files; 7 | 8 | public class ResourcesUtil { 9 | public InputStream getResourceAsStream(String path) throws IOException { 10 | InputStream resourceStream = getClass().getResourceAsStream(path); 11 | if (resourceStream != null) { 12 | return resourceStream; 13 | } 14 | //this will probably work in your IDE, but not from a JAR 15 | File file = new File(path); 16 | return Files.newInputStream(file.toPath()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/resources/legend/by.json: -------------------------------------------------------------------------------- 1 | { 2 | "passed": "паспяховых", 3 | "failed": "якія ўпалі", 4 | "broken": "зламаных", 5 | "unknown": "невядомых", 6 | "skipped": "прапушчаных" 7 | } -------------------------------------------------------------------------------- /allure-notifications-api/src/main/resources/legend/cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "passed": "通过", 3 | "failed": "不通过", 4 | "broken": "损坏", 5 | "unknown": "未知", 6 | "skipped": "跳过" 7 | } -------------------------------------------------------------------------------- /allure-notifications-api/src/main/resources/legend/cnt.json: -------------------------------------------------------------------------------- 1 | { 2 | "passed": "通過", 3 | "failed": "不通過", 4 | "broken": "損壞", 5 | "unknown": "未知", 6 | "skipped": "跳過" 7 | } -------------------------------------------------------------------------------- /allure-notifications-api/src/main/resources/legend/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "passed": "passed", 3 | "failed": "failed", 4 | "broken": "broken", 5 | "unknown": "unknown", 6 | "skipped": "skipped" 7 | } -------------------------------------------------------------------------------- /allure-notifications-api/src/main/resources/legend/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "passed": "passé", 3 | "failed": "manqué", 4 | "broken": "cassé", 5 | "unknown": "inconnu", 6 | "skipped": "ignoré" 7 | } -------------------------------------------------------------------------------- /allure-notifications-api/src/main/resources/legend/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "passed": "успешных", 3 | "failed": "упавших", 4 | "broken": "сломанных", 5 | "unknown": "неизвестных", 6 | "skipped": "пропущенных" 7 | } -------------------------------------------------------------------------------- /allure-notifications-api/src/main/resources/legend/ua.json: -------------------------------------------------------------------------------- 1 | { 2 | "passed": "успішних", 3 | "failed": "невдалих", 4 | "broken": "зламаних", 5 | "unknown": "невідомих", 6 | "skipped": "пропущених" 7 | } -------------------------------------------------------------------------------- /allure-notifications-api/src/main/resources/phrases/by.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": "Вынікі", 3 | "environment": "Працоўнае асяроддзе", 4 | "comment": "Каментар", 5 | "reportAvailableAtLink": "Справаздача даступна па спасылцы", 6 | "scenario": { 7 | "duration": "Працягласць", 8 | "totalScenarios": "Усяго сцэнарыяў", 9 | "totalPassed": "Усяго паспяховых тэстаў", 10 | "totalFailed": "Усяго тэстаў, якія ўпалі", 11 | "totalBroken": "Усяго зламаных тэстаў", 12 | "totalUnknown": "Усяго невядомых тэстаў", 13 | "totalSkipped": "Усяго прапушчаных тэстаў" 14 | }, 15 | "numberOfSuites": "Колькасць тэставых набораў", 16 | "suiteName": "Імя тэставага набору" 17 | } -------------------------------------------------------------------------------- /allure-notifications-api/src/main/resources/phrases/cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": "测试结果", 3 | "environment": "环境", 4 | "comment": "评论", 5 | "reportAvailableAtLink": "测试报告链接", 6 | "scenario": { 7 | "duration": "持续时间", 8 | "totalScenarios": "脚本总数", 9 | "totalPassed": "通过总数", 10 | "totalFailed": "失败总数", 11 | "totalBroken": "损坏总数", 12 | "totalUnknown": "未知错误总数", 13 | "totalSkipped": "跳过总数" 14 | }, 15 | "numberOfSuites": "测试套件数量", 16 | "suiteName": "测试套件名称" 17 | } -------------------------------------------------------------------------------- /allure-notifications-api/src/main/resources/phrases/cnt.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": "測試結果", 3 | "environment": "環境", 4 | "comment": "评论", 5 | "reportAvailableAtLink": "測試報告鏈接", 6 | "scenario": { 7 | "duration": "持續時間", 8 | "totalScenarios": "腳本總數", 9 | "totalPassed": "通過總數", 10 | "totalFailed": "失敗總數", 11 | "totalBroken": "損壞總數", 12 | "totalUnknown": "未知錯誤總數", 13 | "totalSkipped": "跳過總數" 14 | }, 15 | "numberOfSuites": "測試套件名稱", 16 | "suiteName": "測試套件數量" 17 | } -------------------------------------------------------------------------------- /allure-notifications-api/src/main/resources/phrases/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": "Results", 3 | "environment": "Environment", 4 | "comment": "Comment", 5 | "reportAvailableAtLink": "Report available at the link", 6 | "scenario": { 7 | "duration": "Duration", 8 | "totalScenarios": "Total scenarios", 9 | "totalPassed": "Total passed", 10 | "totalFailed": "Total failed", 11 | "totalBroken": "Total broken", 12 | "totalUnknown": "Total unknown", 13 | "totalSkipped": "Total skipped" 14 | }, 15 | "numberOfSuites": "Number of test suites", 16 | "suiteName": "Name of the test suite" 17 | } -------------------------------------------------------------------------------- /allure-notifications-api/src/main/resources/phrases/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": "Résultats", 3 | "environment": "Environnement", 4 | "comment": "Commentaire", 5 | "reportAvailableAtLink": "Rapport disponible par lien", 6 | "scenario": { 7 | "duration": "Durée", 8 | "totalScenarios": "Scénarios totaux", 9 | "totalPassed": "Total passé", 10 | "totalFailed": "Total a échoué", 11 | "totalBroken": "Total cassé", 12 | "totalUnknown": "Total inconnu", 13 | "totalSkipped": "Total ignoré" 14 | }, 15 | "numberOfSuites": "Nombre de suites de tests", 16 | "suiteName": "Nom de la suite de tests" 17 | } -------------------------------------------------------------------------------- /allure-notifications-api/src/main/resources/phrases/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": "Результаты", 3 | "environment": "Рабочее окружение", 4 | "comment": "Комментарий", 5 | "reportAvailableAtLink": "Отчет доступен по ссылке", 6 | "scenario": { 7 | "duration": "Продолжительность", 8 | "totalScenarios": "Всего сценариев", 9 | "totalPassed": "Всего успешных тестов", 10 | "totalFailed": "Всего упавших тестов", 11 | "totalBroken": "Всего сломанных тестов", 12 | "totalUnknown": "Всего неизвестных тестов", 13 | "totalSkipped": "Всего пропущенных тестов" 14 | }, 15 | "numberOfSuites": "Колличество тестовых наборов", 16 | "suiteName": "Имя тестового набора" 17 | } -------------------------------------------------------------------------------- /allure-notifications-api/src/main/resources/phrases/ua.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": "Результати", 3 | "environment": "Середовище", 4 | "comment": "Коментар", 5 | "reportAvailableAtLink": "Звіт доступний за посиланням", 6 | "scenario": { 7 | "duration": "Тривалість", 8 | "totalScenarios": "Усього сценаріїв", 9 | "totalPassed": "Усього успішних тестів", 10 | "totalFailed": "Усього невдалих тестів", 11 | "totalBroken": "Усього зламаних тестів", 12 | "totalUnknown": "Усього невідомих тестів", 13 | "totalSkipped": "Всього пропущених тестів" 14 | }, 15 | "numberOfSuites": "Кількість наборів тестів", 16 | "suiteName": "Назва набору тестів" 17 | } -------------------------------------------------------------------------------- /allure-notifications-api/src/main/resources/templates/html.ftl: -------------------------------------------------------------------------------- 1 | <#import "utils.ftl" as utils> 2 | <#compress> 3 |

${phrases.results}:

4 | ${phrases.environment}: ${environment}
5 | ${phrases.comment}: ${comment}
6 | ${phrases.scenario.duration}: ${time}
7 | ${phrases.scenario.totalScenarios}: ${statistic.total} 8 |
    9 | <#if statistic.passed != 0 > 10 |
  • ${phrases.scenario.totalPassed}: ${statistic.passed} <@utils.printPercentage input=statistic.passed total=statistic.total />
  • 11 | 12 | <#if statistic.failed != 0 > 13 |
  • ${phrases.scenario.totalFailed}: ${statistic.failed} <@utils.printPercentage input=statistic.failed total=statistic.total />
  • 14 | 15 | <#if statistic.broken != 0 > 16 |
  • ${phrases.scenario.totalBroken}: ${statistic.broken}
  • 17 | 18 | <#if statistic.unknown != 0 > 19 |
  • ${phrases.scenario.totalUnknown}: ${statistic.unknown}
  • 20 | 21 | <#if statistic.skipped != 0 > 22 |
  • ${phrases.scenario.totalSkipped}: ${statistic.skipped}
  • 23 | 24 |
25 | 26 | <#if suitesSummaryJson??> 27 | <#assign suitesData = suitesSummaryJson?eval_json> 28 | ${phrases.numberOfSuites}: ${suitesData.total}
29 | <#list suitesData.items as suite> 30 | <#assign suitePassed = suite.statistic.passed> 31 | <#assign suiteFailed = suite.statistic.failed> 32 | <#assign suiteBroken = suite.statistic.broken> 33 | <#assign suiteUnknown = suite.statistic.unknown> 34 | <#assign suiteSkipped = suite.statistic.skipped> 35 | 36 | 37 | 38 | 47 | 48 |
39 | ${phrases.suiteName}: ${suite.name}
40 | ${phrases.scenario.totalScenarios}: ${suite.statistic.total}
41 | <#if suitePassed != 0 >
  • ${phrases.scenario.totalPassed}: ${suitePassed}
  • 42 | <#if suiteFailed != 0 >
  • ${phrases.scenario.totalFailed}: ${suiteFailed}
  • 43 | <#if suiteBroken != 0 >
  • ${phrases.scenario.totalBroken}: ${suiteBroken}
  • 44 | <#if suiteUnknown != 0 >
  • ${phrases.scenario.totalUnknown}: ${suiteUnknown}
  • 45 | <#if suiteSkipped != 0 >
  • ${phrases.scenario.totalSkipped}: ${suiteSkipped}
  • 46 |

    49 | 50 | 51 | <#if reportLink??>${phrases.reportAvailableAtLink}: ${reportLink} 52 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/resources/templates/markdown.ftl: -------------------------------------------------------------------------------- 1 | <#import "utils.ftl" as utils> 2 | <#compress> 3 | *${phrases.results}:* 4 | *${phrases.environment}:* ${environment} 5 | *${phrases.comment}:* ${comment} 6 | *${phrases.scenario.duration}:* ${time} 7 | *${phrases.scenario.totalScenarios}:* ${statistic.total} 8 | <#if statistic.passed != 0 > 9 | *${phrases.scenario.totalPassed}:* ${statistic.passed} <@utils.printPercentage input=statistic.passed total=statistic.total /> 10 | 11 | <#if statistic.failed != 0 > 12 | *${phrases.scenario.totalFailed}:* ${statistic.failed} <@utils.printPercentage input=statistic.failed total=statistic.total /> 13 | 14 | <#if statistic.broken != 0 > 15 | *${phrases.scenario.totalBroken}:* ${statistic.broken} 16 | 17 | <#if statistic.unknown != 0 > 18 | *${phrases.scenario.totalUnknown}:* ${statistic.unknown} 19 | 20 | <#if statistic.skipped != 0 > 21 | *${phrases.scenario.totalSkipped}:* ${statistic.skipped} 22 | 23 | 24 | <#if suitesSummaryJson??> 25 | <#assign suitesData = suitesSummaryJson?eval_json> 26 | *${phrases.numberOfSuites}:* ${suitesData.total} 27 | <#list suitesData.items as suite> 28 | <#assign suitePassed = suite.statistic.passed> 29 | <#assign suiteFailed = suite.statistic.failed> 30 | <#assign suiteBroken = suite.statistic.broken> 31 | <#assign suiteUnknown = suite.statistic.unknown> 32 | <#assign suiteSkipped = suite.statistic.skipped> 33 | 34 | *${phrases.suiteName}:* ${suite.name} 35 | > *${phrases.scenario.totalScenarios}:* ${suite.statistic.total} 36 | <#if suitePassed != 0 >> *${phrases.scenario.totalPassed}:* ${suitePassed} 37 | <#if suiteFailed != 0 >> *${phrases.scenario.totalFailed}:* ${suiteFailed} 38 | <#if suiteBroken != 0 >> *${phrases.scenario.totalBroken}:* ${suiteBroken} 39 | <#if suiteUnknown != 0 >> *${phrases.scenario.totalUnknown}:* ${suiteUnknown} 40 | <#if suiteSkipped != 0 >> *${phrases.scenario.totalSkipped}:* ${suiteSkipped} 41 | 42 | 43 | <#if reportLink??>*${phrases.reportAvailableAtLink}:* ${reportLink} 44 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/resources/templates/rocket.ftl: -------------------------------------------------------------------------------- 1 | <#import "utils.ftl" as utils> 2 | <#compress> 3 | **${phrases.results}:** 4 | **-${phrases.environment}:** ${environment} 5 | **-${phrases.comment}:** ${comment} 6 | **-${phrases.scenario.duration}:** **${time}** 7 | **-${phrases.scenario.totalScenarios}:** ${statistic.total} 8 | <#if statistic.passed != 0 > 9 | **-${phrases.scenario.totalPassed}:** ${statistic.passed} **<@utils.printPercentage input=statistic.passed total=statistic.total />** 10 | 11 | <#if statistic.failed != 0 > 12 | **-${phrases.scenario.totalFailed}:** ${statistic.failed} **<@utils.printPercentage input=statistic.failed total=statistic.total />** 13 | 14 | <#if statistic.broken != 0 > 15 | **-${phrases.scenario.totalBroken}:** ${statistic.broken} 16 | 17 | <#if statistic.unknown != 0 > 18 | **-${phrases.scenario.totalUnknown}:** ${statistic.unknown} 19 | 20 | <#if statistic.skipped != 0 > 21 | **-${phrases.scenario.totalSkipped}:** ${statistic.skipped} 22 | 23 | 24 | <#if suitesSummaryJson??> 25 | <#assign suitesData = suitesSummaryJson?eval_json> 26 | **-${phrases.numberOfSuites}:** **${suitesData.total}** 27 | <#list suitesData.items as suite> 28 | <#assign suitePassed = suite.statistic.passed> 29 | <#assign suiteFailed = suite.statistic.failed> 30 | <#assign suiteBroken = suite.statistic.broken> 31 | <#assign suiteUnknown = suite.statistic.unknown> 32 | <#assign suiteSkipped = suite.statistic.skipped> 33 | 34 | **-${phrases.suiteName}:** **${suite.name}** 35 | **-${phrases.scenario.totalScenarios}:** **${suite.statistic.total}** 36 | <#if suitePassed != 0 > **-${phrases.scenario.totalPassed}:** ${suitePassed} 37 | <#if suiteFailed != 0 > **-${phrases.scenario.totalFailed}:** ${suiteFailed} 38 | <#if suiteBroken != 0 > **-${phrases.scenario.totalBroken}:** ${suiteBroken} 39 | <#if suiteUnknown != 0 > **-${phrases.scenario.totalUnknown}:** ${suiteUnknown} 40 | <#if suiteSkipped != 0 > **-${phrases.scenario.totalSkipped}:** ${suiteSkipped} 41 | 42 | 43 | <#if reportLink??>**${phrases.reportAvailableAtLink}:** ${reportLink} 44 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/resources/templates/telegram.ftl: -------------------------------------------------------------------------------- 1 | <#import "utils.ftl" as utils> 2 | <#compress> 3 | ${phrases.results}: 4 | ${phrases.environment}: ${environment} 5 | ${phrases.comment}: ${comment} 6 | ${phrases.scenario.duration}: ${time} 7 | ${phrases.scenario.totalScenarios}: ${statistic.total} 8 | <#if statistic.passed != 0 > 9 | ${phrases.scenario.totalPassed}: ${statistic.passed} <@utils.printPercentage input=statistic.passed total=statistic.total /> 10 | 11 | <#if statistic.failed != 0 > 12 | ${phrases.scenario.totalFailed}: ${statistic.failed} <@utils.printPercentage input=statistic.failed total=statistic.total /> 13 | 14 | <#if statistic.broken != 0 > 15 | ${phrases.scenario.totalBroken}: ${statistic.broken} 16 | 17 | <#if statistic.unknown != 0 > 18 | ${phrases.scenario.totalUnknown}: ${statistic.unknown} 19 | 20 | <#if statistic.skipped != 0 > 21 | ${phrases.scenario.totalSkipped}: ${statistic.skipped} 22 | 23 | 24 | <#if suitesSummaryJson??> 25 | <#assign suitesData = suitesSummaryJson?eval_json> 26 | ${phrases.numberOfSuites}: ${suitesData.total} 27 | <#list suitesData.items as suite> 28 | <#assign suitePassed = suite.statistic.passed> 29 | <#assign suiteFailed = suite.statistic.failed> 30 | <#assign suiteBroken = suite.statistic.broken> 31 | <#assign suiteUnknown = suite.statistic.unknown> 32 | <#assign suiteSkipped = suite.statistic.skipped> 33 | 34 | ${phrases.suiteName}: ${suite.name} 35 | ${phrases.scenario.totalScenarios}: ${suite.statistic.total} 36 | <#if suitePassed != 0 >${phrases.scenario.totalPassed}: ${suitePassed} 37 | <#if suiteFailed != 0 >${phrases.scenario.totalFailed}: ${suiteFailed} 38 | <#if suiteBroken != 0 >${phrases.scenario.totalBroken}: ${suiteBroken} 39 | <#if suiteUnknown != 0 >${phrases.scenario.totalUnknown}: ${suiteUnknown} 40 | <#if suiteSkipped != 0 >${phrases.scenario.totalSkipped}: ${suiteSkipped} 41 | 42 | 43 | <#if reportLink??>${phrases.reportAvailableAtLink}: ${reportLink} 44 | -------------------------------------------------------------------------------- /allure-notifications-api/src/main/resources/templates/utils.ftl: -------------------------------------------------------------------------------- 1 | <#macro printPercentage input total> 2 | (${( (input * 100.00) / total )?string("##.#")} %)<#t> 3 | 4 | -------------------------------------------------------------------------------- /allure-notifications-api/src/test/java/guru/qa/allure/notifications/clients/NotificationTests.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.clients; 2 | 3 | import static org.mockito.ArgumentMatchers.any; 4 | import static org.mockito.ArgumentMatchers.eq; 5 | import static org.mockito.Mockito.mockConstruction; 6 | import static org.mockito.Mockito.mockStatic; 7 | import static org.mockito.Mockito.never; 8 | import static org.mockito.Mockito.times; 9 | import static org.mockito.Mockito.verify; 10 | import static org.mockito.Mockito.when; 11 | 12 | import java.io.File; 13 | import java.io.IOException; 14 | import java.nio.file.Files; 15 | import java.nio.file.Path; 16 | import java.nio.file.Paths; 17 | import java.util.Collections; 18 | import java.util.stream.Stream; 19 | 20 | import org.junit.jupiter.api.extension.ExtendWith; 21 | import org.junit.jupiter.params.ParameterizedTest; 22 | import org.junit.jupiter.params.provider.Arguments; 23 | import org.junit.jupiter.params.provider.CsvSource; 24 | import org.junit.jupiter.params.provider.MethodSource; 25 | import org.mockito.Mock; 26 | import org.mockito.MockedConstruction; 27 | import org.mockito.MockedStatic; 28 | import org.mockito.junit.jupiter.MockitoExtension; 29 | 30 | import guru.qa.allure.notifications.chart.Chart; 31 | import guru.qa.allure.notifications.config.Config; 32 | import guru.qa.allure.notifications.config.base.Base; 33 | import guru.qa.allure.notifications.config.enums.Language; 34 | import guru.qa.allure.notifications.exceptions.MessagingException; 35 | import guru.qa.allure.notifications.json.JSON; 36 | import guru.qa.allure.notifications.model.legend.Legend; 37 | import guru.qa.allure.notifications.model.phrases.Phrases; 38 | import guru.qa.allure.notifications.model.summary.Summary; 39 | import guru.qa.allure.notifications.template.data.MessageData; 40 | 41 | @ExtendWith(MockitoExtension.class) 42 | class NotificationTests { 43 | private static final String SUITES_PATH = "widgets/suites.json"; 44 | private static final String PHRASES_PATH = "/phrases/en.json"; 45 | private static final String LEGEND_PATH = "/legend/en.json"; 46 | private static final String ALLURE_PATH = "/allure"; 47 | 48 | private static final Summary SUMMARY = new Summary(); 49 | private static final Phrases PHRASES = new Phrases(); 50 | 51 | @Mock private Notifier notifier; 52 | @Mock private Path path; 53 | 54 | static Stream inputData() { 55 | return Stream.of( 56 | Arguments.of(false, null, 0, 1), 57 | Arguments.of(true, new Legend(), 1, 0) 58 | ); 59 | } 60 | 61 | @ParameterizedTest 62 | @MethodSource("inputData") 63 | void shouldSendNotification(boolean chartEnabled, Legend legend, int chartActionsCount, int textActionsCount) 64 | throws MessagingException, IOException { 65 | Config config = getConfig(chartEnabled, true); 66 | Base base = config.getBase(); 67 | byte[] chartImg = new byte[]{}; 68 | 69 | try (MockedConstruction jsonUtilsMock = mockConstruction(JSON.class, 70 | (mock, context) -> { 71 | when(mock.parseFile(any(File.class), eq(Summary.class))).thenReturn(SUMMARY); 72 | when(mock.parseResource(PHRASES_PATH, Phrases.class)).thenReturn(PHRASES); 73 | when(mock.parseResource(LEGEND_PATH, Legend.class)).thenReturn(legend); 74 | }); 75 | MockedStatic clientFactoryMock = mockStatic(ClientFactory.class); 76 | MockedStatic pathsMock = mockStatic(Paths.class); 77 | MockedStatic filesMock = mockStatic(Files.class); 78 | MockedStatic chartMock = mockStatic(Chart.class)) { 79 | 80 | clientFactoryMock.when(() -> ClientFactory.from(config)).thenReturn(Collections.singletonList(notifier)); 81 | pathsMock.when(() -> Paths.get(ALLURE_PATH, SUITES_PATH)).thenReturn(path); 82 | chartMock.when(() -> Chart.createChart(base, SUMMARY, legend)).thenReturn(chartImg); 83 | 84 | Notification.send(config); 85 | 86 | clientFactoryMock.verify(() -> ClientFactory.from(config)); 87 | JSON json = jsonUtilsMock.constructed().get(0); 88 | verify(json).parseResource(PHRASES_PATH, Phrases.class); 89 | verify(json, times(chartActionsCount)).parseResource(LEGEND_PATH, Legend.class); 90 | chartMock.verify(() -> Chart.createChart(base, SUMMARY, legend), times(chartActionsCount)); 91 | filesMock.verify(() -> Files.exists(path)); 92 | filesMock.verify(() -> Files.readAllBytes(path), never()); 93 | verify(notifier, times(chartActionsCount)).sendPhoto(any(MessageData.class), eq(chartImg)); 94 | verify(notifier, times(textActionsCount)).sendText(any(MessageData.class)); 95 | } 96 | } 97 | 98 | @ParameterizedTest 99 | @CsvSource({ 100 | "true, 1", 101 | "false, 0" 102 | }) 103 | void shouldSendNotificationWithSuitesData(boolean enableSuitesPublishing, int suitesActionsCount) 104 | throws MessagingException, IOException { 105 | Config config = getConfig(false, enableSuitesPublishing); 106 | 107 | try (MockedConstruction jsonUtilsMock = mockConstruction(JSON.class); 108 | MockedStatic clientFactoryMock = mockStatic(ClientFactory.class); 109 | MockedStatic pathsMock = mockStatic(Paths.class); 110 | MockedStatic filesMock = mockStatic(Files.class)) { 111 | clientFactoryMock.when(() -> ClientFactory.from(config)).thenReturn(Collections.singletonList(notifier)); 112 | pathsMock.when(() -> Paths.get(ALLURE_PATH, SUITES_PATH)).thenReturn(path); 113 | filesMock.when(() -> Files.exists(path)).thenReturn(true); 114 | filesMock.when(() -> Files.readAllBytes(path)).thenReturn(new byte[]{}); 115 | 116 | Notification.send(config); 117 | 118 | filesMock.verify(() -> Files.exists(path), times(suitesActionsCount)); 119 | filesMock.verify(() -> Files.readAllBytes(path), times(suitesActionsCount)); 120 | } 121 | } 122 | 123 | private Config getConfig(boolean enableChart, boolean enableSuitesPublishing) { 124 | Config config = new Config(); 125 | Base base = new Base(); 126 | base.setAllureFolder(ALLURE_PATH); 127 | base.setLanguage(Language.en); 128 | base.setEnableChart(enableChart); 129 | base.setEnableSuitesPublishing(enableSuitesPublishing); 130 | config.setBase(base); 131 | return config; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /allure-notifications-api/src/test/java/guru/qa/allure/notifications/clients/mail/EmailTests.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.clients.mail; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | import static org.mockito.ArgumentMatchers.anyString; 6 | import static org.mockito.Mockito.lenient; 7 | import static org.mockito.Mockito.mockConstruction; 8 | import static org.mockito.Mockito.verify; 9 | 10 | import java.util.function.Consumer; 11 | 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.api.extension.ExtendWith; 15 | import org.mockito.Mock; 16 | import org.mockito.MockedConstruction; 17 | import org.mockito.junit.jupiter.MockitoExtension; 18 | 19 | import guru.qa.allure.notifications.config.base.Base; 20 | import guru.qa.allure.notifications.config.mail.Mail; 21 | import guru.qa.allure.notifications.exceptions.MessagingException; 22 | import guru.qa.allure.notifications.template.data.MessageData; 23 | 24 | @ExtendWith(MockitoExtension.class) 25 | class EmailTests { 26 | private static final String FROM = "testFROM@gmail.com"; 27 | private static final String TO = "testTO@gmail.com"; 28 | private static final String CC = "testCC@gmail.com, testCC2@gmail.com"; 29 | private static final String BCC = "testBCC@gmail.com"; 30 | private static final String PROJECT = "LETTER PROJECT"; 31 | private static final String EMPTY_TEMPLATE_PATH = "/template/emptyTemplate.ftl"; 32 | private static final String TEXT_FROM_TEMPLATE = "for test purposes"; 33 | private static final byte[] IMG = new byte[1]; 34 | 35 | private static final Base BASE = new Base(); 36 | 37 | @Mock private Letter letter; 38 | @Mock private MessageData messageData; 39 | 40 | @BeforeEach 41 | void beforeEach() { 42 | BASE.setProject(PROJECT); 43 | lenient().when(messageData.getProject()).thenReturn(PROJECT); 44 | } 45 | 46 | private void mockLetter(Consumer letterConsumer) { 47 | try (MockedConstruction letterMock = mockConstruction(Letter.class, 48 | (mock, context) -> { 49 | lenient().when(mock.from(FROM)).thenReturn(letter); 50 | lenient().when(letter.to(TO)).thenReturn(letter); 51 | lenient().when(letter.cc(CC)).thenReturn(letter); 52 | lenient().when(letter.bcc(BCC)).thenReturn(letter); 53 | lenient().when(letter.subject(PROJECT)).thenReturn(letter); 54 | lenient().when(letter.text(anyString())).thenReturn(letter); 55 | lenient().when(letter.image(IMG)).thenReturn(letter); 56 | })) { 57 | letterConsumer.accept(letter); 58 | } 59 | } 60 | 61 | @Test 62 | void shouldSendText() { 63 | Mail mail = createMailWithoutRecipient(); 64 | mail.setTo(TO); 65 | 66 | mockLetter(l -> { 67 | Email email = new Email(mail); 68 | try { 69 | email.sendText(messageData); 70 | verify(l).to(TO); 71 | verify(l).cc(CC); 72 | verify(l).bcc(BCC); 73 | verify(l).subject(PROJECT); 74 | verify(l).text(TEXT_FROM_TEMPLATE); 75 | verify(l).send(); 76 | } catch (MessagingException e) { 77 | throw new RuntimeException(e); 78 | } 79 | }); 80 | } 81 | 82 | @Test 83 | void shouldSendTextWithDeprecatedRecipientField() { 84 | Mail mail = createMailWithoutRecipient(); 85 | mail.setRecipient(TO); 86 | 87 | mockLetter(l -> { 88 | Email email = new Email(mail); 89 | try { 90 | email.sendText(messageData); 91 | verify(l).to(TO); 92 | } catch (MessagingException e) { 93 | throw new RuntimeException(e); 94 | } 95 | }); 96 | } 97 | 98 | @Test 99 | void shouldThrowExceptionIfAmbiguousRecipientFieldsUsing() { 100 | Mail mail = createMailWithoutRecipient(); 101 | mail.setRecipient(TO); 102 | mail.setTo(TO); 103 | 104 | mockLetter(l -> { 105 | Email email = new Email(mail); 106 | Exception exception = assertThrows(IllegalArgumentException.class, () -> email.sendText(messageData)); 107 | assertEquals("Ambiguous configuration fields \"recipient\" and \"to\" found, " 108 | + "use only \"to\" field instead.", exception.getMessage()); 109 | }); 110 | } 111 | 112 | @Test 113 | void shouldSendTextWithChart() { 114 | Mail mail = createMailWithoutRecipient(); 115 | mail.setTo(TO); 116 | 117 | mockLetter(l -> { 118 | Email email = new Email(mail); 119 | try { 120 | email.sendPhoto(messageData, IMG); 121 | verify(l).to(TO); 122 | verify(l).cc(CC); 123 | verify(l).bcc(BCC); 124 | verify(l).subject(PROJECT); 125 | verify(l).text("
    " + TEXT_FROM_TEMPLATE); 126 | verify(l).image(IMG); 127 | verify(l).send(); 128 | } catch (MessagingException e) { 129 | throw new RuntimeException(e); 130 | } 131 | }); 132 | } 133 | 134 | private Mail createMailWithoutRecipient() { 135 | Mail mail = new Mail(); 136 | mail.setTemplatePath(EMPTY_TEMPLATE_PATH); 137 | mail.setFrom(FROM); 138 | mail.setCc(CC); 139 | mail.setBcc(BCC); 140 | return mail; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /allure-notifications-api/src/test/java/guru/qa/allure/notifications/clients/mail/LetterTests.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.clients.mail; 2 | 3 | import static org.mockito.Mockito.mockConstruction; 4 | import static org.mockito.Mockito.verify; 5 | import static jakarta.mail.Message.RecipientType; 6 | import static org.mockito.Mockito.verifyNoInteractions; 7 | 8 | import java.util.function.Consumer; 9 | 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Test; 12 | import org.junit.jupiter.api.extension.ExtendWith; 13 | import org.junit.jupiter.params.ParameterizedTest; 14 | import org.junit.jupiter.params.provider.NullAndEmptySource; 15 | import org.mockito.MockedConstruction; 16 | import org.mockito.junit.jupiter.MockitoExtension; 17 | 18 | import guru.qa.allure.notifications.config.mail.Mail; 19 | import guru.qa.allure.notifications.exceptions.MessageBuildException; 20 | import guru.qa.allure.notifications.util.MailUtil; 21 | import jakarta.mail.Message; 22 | import jakarta.mail.MessagingException; 23 | import jakarta.mail.internet.MimeMessage; 24 | 25 | @ExtendWith(MockitoExtension.class) 26 | class LetterTests { 27 | private static final String CC = "testCC1@gmail.com, testCC2@gmail.com"; 28 | private static final String BCC = "testBCC@gmail.com"; 29 | 30 | private final Mail mail = new Mail(); 31 | private Letter letter; 32 | 33 | @BeforeEach 34 | void beforeEach() { 35 | mail.setFrom("from"); 36 | mail.setHost("host"); 37 | mail.setPort("port"); 38 | } 39 | 40 | private void mockMessage(Consumer messageConsumer) { 41 | try (MockedConstruction messageMock = mockConstruction(MimeMessage.class)) { 42 | letter = new Letter(mail); 43 | Message message = messageMock.constructed().get(0); 44 | messageConsumer.accept(message); 45 | } 46 | } 47 | 48 | @Test 49 | void shouldSetCcBccIfPresent() { 50 | mockMessage(m -> { 51 | try { 52 | letter.cc(CC); 53 | letter.bcc(BCC); 54 | verify(m).setRecipients(RecipientType.CC, MailUtil.recipients(CC)); 55 | verify(m).setRecipients(RecipientType.BCC, MailUtil.recipients(BCC)); 56 | } catch (MessagingException | MessageBuildException e) { 57 | throw new RuntimeException(e); 58 | } 59 | }); 60 | } 61 | 62 | @ParameterizedTest 63 | @NullAndEmptySource 64 | void shouldNotSetCcBccIfNotPresent(String value) { 65 | mockMessage(m -> { 66 | try { 67 | letter.bcc(value); 68 | letter.cc(value); 69 | } catch (MessageBuildException e) { 70 | throw new RuntimeException(e); 71 | } 72 | verifyNoInteractions(m); 73 | }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /allure-notifications-api/src/test/java/guru/qa/allure/notifications/clients/skype/SkypeClientTest.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.clients.skype; 2 | 3 | import guru.qa.allure.notifications.config.skype.Skype; 4 | 5 | import org.apache.commons.lang3.reflect.MethodUtils; 6 | import org.junit.jupiter.api.extension.ExtendWith; 7 | import org.junit.jupiter.params.ParameterizedTest; 8 | import org.junit.jupiter.params.provider.CsvSource; 9 | import org.mockito.InjectMocks; 10 | import org.mockito.Mock; 11 | import org.mockito.Mockito; 12 | import org.mockito.junit.jupiter.MockitoExtension; 13 | 14 | import static org.junit.jupiter.api.Assertions.assertEquals; 15 | 16 | @ExtendWith(MockitoExtension.class) 17 | class SkypeClientTest { 18 | @Mock private Skype skype; 19 | @InjectMocks private SkypeClient app; 20 | 21 | @ParameterizedTest(name = "Get host name from service url: {0}") 22 | @CsvSource({ 23 | "smba.trafficmanager.net/apis/,smba.trafficmanager.net", 24 | "smba.net/apis/,smba.net", 25 | "smba.trafficmanager.net/,smba.trafficmanager.net", 26 | "smba.trafficmanager.net,smba.trafficmanager.net", 27 | 28 | }) 29 | void hostTest(String url, String expected) throws ReflectiveOperationException { 30 | Mockito.when(skype.getServiceUrl()).thenReturn(url); 31 | assertEquals(expected, MethodUtils.invokeMethod(app, true, "host")); 32 | } 33 | } -------------------------------------------------------------------------------- /allure-notifications-api/src/test/java/guru/qa/allure/notifications/clients/telegram/TelegramClientTest.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.clients.telegram; 2 | 3 | import guru.qa.allure.notifications.config.telegram.Telegram; 4 | import guru.qa.allure.notifications.exceptions.MessagingException; 5 | import guru.qa.allure.notifications.template.data.MessageData; 6 | import kong.unirest.MatchStatus; 7 | import kong.unirest.MockClient; 8 | 9 | import org.junit.jupiter.api.Test; 10 | 11 | import java.util.List; 12 | 13 | import static kong.unirest.HttpMethod.POST; 14 | import static org.mockito.Mockito.RETURNS_DEEP_STUBS; 15 | import static org.mockito.Mockito.mock; 16 | 17 | class TelegramClientTest { 18 | 19 | private static final String EMPTY_TEMPLATE_PATH = "/template/emptyTemplate.ftl"; 20 | 21 | private final MockClient mock = MockClient.register(); 22 | private final MessageData messageData = mock(MessageData.class, RETURNS_DEEP_STUBS); 23 | 24 | @Test 25 | void topicIsSetTest() throws MessagingException { 26 | Telegram telegram = createConfig("topic"); 27 | 28 | new TelegramClient(telegram).sendText(messageData); 29 | 30 | mock.assertThat(POST, "https://api.telegram.org/bottoken/sendMessage") 31 | .hadField("message_thread_id", "topic"); 32 | } 33 | 34 | @Test 35 | void topicIsOptionalTest() throws MessagingException { 36 | Telegram telegram = createConfig(null); 37 | 38 | mock.expect(POST, "https://api.telegram.org/bottoken/sendMessage") 39 | .body(TelegramClientTest::hasNoMessageThreadIdParameter); 40 | 41 | new TelegramClient(telegram).sendText(messageData); 42 | 43 | mock.verifyAll(); 44 | } 45 | 46 | @Test 47 | void topicIsSetForPhotoTest() throws MessagingException { 48 | Telegram telegram = createConfig("topic"); 49 | 50 | new TelegramClient(telegram).sendPhoto(messageData, new byte[0]); 51 | 52 | mock.assertThat(POST, "https://api.telegram.org/bottoken/sendPhoto") 53 | .hadField("message_thread_id", "topic"); 54 | } 55 | 56 | @Test 57 | void topicIsNotSetForPhotoTest() throws MessagingException { 58 | Telegram telegram = createConfig(null); 59 | 60 | mock.expect(POST, "https://api.telegram.org/bottoken/sendPhoto") 61 | .body(TelegramClientTest::hasNoMessageThreadIdParameter); 62 | 63 | new TelegramClient(telegram).sendPhoto(messageData, new byte[0]); 64 | 65 | mock.verifyAll(); 66 | } 67 | 68 | private static MatchStatus hasNoMessageThreadIdParameter(List body) { 69 | return new MatchStatus( 70 | body.stream() 71 | .map(x -> x.split("=")[0]) 72 | .noneMatch(formParameterName -> formParameterName.equals("message_thread_id")), 73 | "No parameter 'message_thread_id' was expected, but found"); 74 | } 75 | 76 | private static Telegram createConfig(String topic) { 77 | Telegram telegram = new Telegram(); 78 | telegram.setChat("chat"); 79 | telegram.setTopic(topic); 80 | telegram.setToken("token"); 81 | telegram.setReplyTo("reply-to"); 82 | telegram.setTemplatePath(EMPTY_TEMPLATE_PATH); 83 | return telegram; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /allure-notifications-api/src/test/java/guru/qa/allure/notifications/config/ApplicationConfigTest.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.config; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude.Include; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.databind.json.JsonMapper; 6 | 7 | import net.javacrumbs.jsonunit.core.Option; 8 | import net.javacrumbs.jsonunit.core.internal.Options; 9 | 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.params.ParameterizedTest; 12 | import org.junit.jupiter.params.provider.MethodSource; 13 | import org.junitpioneer.jupiter.ClearSystemProperty; 14 | import org.junitpioneer.jupiter.SetSystemProperty; 15 | 16 | import java.io.FileNotFoundException; 17 | import java.io.IOException; 18 | import java.nio.charset.StandardCharsets; 19 | import java.nio.file.Files; 20 | import java.nio.file.Paths; 21 | import java.util.HashMap; 22 | import java.util.stream.Stream; 23 | 24 | import static net.javacrumbs.jsonunit.JsonMatchers.jsonEquals; 25 | import static org.hamcrest.MatcherAssert.assertThat; 26 | import static org.junit.jupiter.api.Assertions.assertAll; 27 | import static org.junit.jupiter.api.Assertions.assertEquals; 28 | import static org.junit.jupiter.api.Assertions.assertThrows; 29 | 30 | class ApplicationConfigTest { 31 | private static final String CONFIG_FILE_PROPERTY = "configFile"; 32 | 33 | public static Stream configFiles() { 34 | return Stream.of( 35 | "src/test/resources/data/testConfig.json", 36 | "src/test/resources/data/testConfig_noData.json" 37 | ); 38 | } 39 | 40 | @Test 41 | @ClearSystemProperty(key = CONFIG_FILE_PROPERTY) 42 | void noConfigSpecifiedTest() { 43 | assertThrows(IllegalArgumentException.class, 44 | ApplicationConfig::new, 45 | "'configFile' property is not set or empty: null"); 46 | } 47 | 48 | @Test 49 | @SetSystemProperty(key = CONFIG_FILE_PROPERTY, value = "wrong/config.path") 50 | void wrongConfigPathSpecifiedTest() { 51 | ApplicationConfig applicationConfig = new ApplicationConfig(); 52 | assertThrows(FileNotFoundException.class, applicationConfig::readConfig); 53 | } 54 | 55 | @ParameterizedTest 56 | @MethodSource("configFiles") 57 | void noPropertiesAreLoadedTest(String configFilePath) throws IOException { 58 | String configFromFile = new String(Files.readAllBytes(Paths.get(configFilePath)), StandardCharsets.UTF_8); 59 | Config config = new ApplicationConfig(configFilePath).readConfig(); 60 | 61 | ObjectMapper jsonMapper = new JsonMapper().setSerializationInclusion(Include.NON_NULL); 62 | String parsedConfig = jsonMapper.writeValueAsString(config); 63 | assertThat(parsedConfig, 64 | jsonEquals(configFromFile).withOptions(Options.empty().with(Option.IGNORING_EXTRA_FIELDS))); 65 | } 66 | 67 | @ParameterizedTest 68 | @MethodSource("configFiles") 69 | @SetSystemProperty(key = "notifications.base.environment", value = "environmentOverride") 70 | @SetSystemProperty(key = "notifications.base.comment", value = "commentOverride") 71 | @SetSystemProperty(key = "notifications.base.allureFolder", value = "allureFolderOverride") 72 | @SetSystemProperty(key = "notifications.base.project", value = "projectOverride") 73 | @SetSystemProperty(key = "notifications.base.reportLink", value = "reportLinkOverride") 74 | @SetSystemProperty(key = "notifications.base.logo", value = "logoOverride") 75 | @SetSystemProperty(key = "notifications.base.durationFormat", value = "durationFormatOverride") 76 | @SetSystemProperty(key = "notifications.base.customData.variable1", value = "customDataOverride_1") 77 | @SetSystemProperty(key = "notifications.base.customData.variable2", value = "") 78 | @SetSystemProperty(key = "notifications.base.customData.v", value = "newVariable") 79 | @SetSystemProperty(key = "notifications.base.customData.", value = "key_can_be_empty") 80 | @SetSystemProperty(key = "notifications.telegram.token", value = "tokenOverride") 81 | @SetSystemProperty(key = "notifications.telegram.chat", value = "chatOverride") 82 | @SetSystemProperty(key = "notifications.telegram.topic", value = "topicOverride") 83 | @SetSystemProperty(key = "notifications.telegram.replyTo", value = "replyToOverride") 84 | @SetSystemProperty(key = "notifications.telegram.templatePath", value = "templatePathOverride") 85 | @SetSystemProperty(key = "notifications.skype.appId", value = "appIdOverride") 86 | void propertiesAreOverloadedTest(String configFilePath) throws IOException { 87 | Config config = new ApplicationConfig(configFilePath).readConfig(); 88 | 89 | assertAll( 90 | () -> assertEquals("environmentOverride", config.getBase().getEnvironment()), 91 | () -> assertEquals("commentOverride", config.getBase().getComment()), 92 | () -> assertEquals("allureFolderOverride", config.getBase().getAllureFolder()), 93 | () -> assertEquals("projectOverride", config.getBase().getProject()), 94 | () -> assertEquals("reportLinkOverride", config.getBase().getReportLink()), 95 | () -> assertEquals("logoOverride", config.getBase().getLogo()), 96 | () -> assertEquals("durationFormatOverride", config.getBase().getDurationFormat()), 97 | () -> assertEquals(new HashMap() { 98 | { 99 | put("variable1", "customDataOverride_1"); 100 | put("variable2", ""); 101 | put("v", "newVariable"); 102 | put("", "key_can_be_empty"); 103 | } 104 | }, config.getBase().getCustomData()), 105 | 106 | () -> assertEquals("tokenOverride", config.getTelegram().getToken()), 107 | () -> assertEquals("chatOverride", config.getTelegram().getChat()), 108 | () -> assertEquals("topicOverride", config.getTelegram().getTopic()), 109 | () -> assertEquals("replyToOverride", config.getTelegram().getReplyTo()), 110 | () -> assertEquals("templatePathOverride", config.getTelegram().getTemplatePath()), 111 | 112 | () -> assertEquals("appIdOverride", config.getSkype().getAppId()) 113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /allure-notifications-api/src/test/java/guru/qa/allure/notifications/formatters/FormattersTests.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.formatters; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import guru.qa.allure.notifications.config.base.Base; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class FormattersTests { 9 | @Test 10 | void shouldFormatDurationUsingDefaultConfig() { 11 | String durationAsString = Formatters.formatDuration(45296789L, new Base().getDurationFormat()); 12 | assertEquals("12:34:56.789", durationAsString); 13 | } 14 | 15 | @Test 16 | void shouldFormatDurationUsingCustomFormat() { 17 | String durationAsString = Formatters.formatDuration(45296789L, "HH:mm:ss"); 18 | assertEquals("12:34:56", durationAsString); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /allure-notifications-api/src/test/java/guru/qa/allure/notifications/template/MessageTemplateTests.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.template; 2 | 3 | import static java.lang.ClassLoader.getSystemResource; 4 | import static java.nio.charset.StandardCharsets.UTF_8; 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | import static org.junit.jupiter.params.provider.Arguments.arguments; 7 | 8 | import java.io.IOException; 9 | import java.net.URISyntaxException; 10 | import java.net.URL; 11 | import java.nio.file.Files; 12 | import java.nio.file.Paths; 13 | import java.util.stream.Stream; 14 | 15 | import org.junit.jupiter.params.ParameterizedTest; 16 | import org.junit.jupiter.params.provider.Arguments; 17 | import org.junit.jupiter.params.provider.MethodSource; 18 | 19 | import guru.qa.allure.notifications.config.Config; 20 | import guru.qa.allure.notifications.exceptions.MessageBuildException; 21 | import guru.qa.allure.notifications.json.JSON; 22 | import guru.qa.allure.notifications.model.phrases.Phrases; 23 | import guru.qa.allure.notifications.model.summary.Summary; 24 | import guru.qa.allure.notifications.template.data.MessageData; 25 | 26 | class MessageTemplateTests { 27 | 28 | static Stream testData() { 29 | return Stream.of( 30 | arguments("templates/html.ftl", "messages/html.txt"), 31 | arguments("templates/markdown.ftl", "messages/markdown.txt"), 32 | arguments("templates/rocket.ftl", "messages/rocket.txt"), 33 | arguments("templates/telegram.ftl", "messages/telegram.txt"), 34 | arguments("template/testTemplateAsResource.ftl", "messages/customHtml.txt"), 35 | arguments(getSystemResource("template/testTemplateAsFile.ftl").getFile(), "messages/customHtml.txt") 36 | ); 37 | } 38 | 39 | @ParameterizedTest 40 | @MethodSource("testData") 41 | void shouldValidateGeneratedMessageFromTemplate(String templatePath, String expectedMessagePath) 42 | throws IOException, URISyntaxException, MessageBuildException { 43 | 44 | Config config = new JSON().parseResource("/data/testConfig.json", Config.class); 45 | Summary summary = new JSON().parseResource("/data/testSummary.json", Summary.class); 46 | String suitesSummaryJson = readResource("data/testSuites.json"); 47 | Phrases phrases = new JSON().parseResource("/phrases/en.json", Phrases.class); 48 | 49 | MessageData messageData = new MessageData(config.getBase(), summary, suitesSummaryJson, phrases); 50 | 51 | String messageGeneratedFromTemplate = MessageTemplate.createMessageFromTemplate(messageData, templatePath); 52 | String expectedMessage = readResource(expectedMessagePath); 53 | assertEquals(expectedMessage, messageGeneratedFromTemplate); 54 | } 55 | 56 | private String readResource(String name) throws IOException, URISyntaxException { 57 | URL resourceUrl = getClass().getClassLoader().getResource(name); 58 | return new String(Files.readAllBytes(Paths.get(resourceUrl.toURI())), UTF_8); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /allure-notifications-api/src/test/java/guru/qa/allure/notifications/util/MailPropertiesTests.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.util; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.util.Properties; 6 | 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.params.ParameterizedTest; 9 | import org.junit.jupiter.params.provider.CsvSource; 10 | 11 | import guru.qa.allure.notifications.config.mail.Mail; 12 | import guru.qa.allure.notifications.config.mail.SecurityProtocol; 13 | 14 | class MailPropertiesTests { 15 | 16 | private static final String FROM = "from@gmail.com"; 17 | private static final String HOST = "smtp.gmail.com"; 18 | private static final String PORT = "465"; 19 | 20 | @Test 21 | void shouldCreateMailPropertiesWithoutSecurityProtocolSpecified() { 22 | 23 | Mail mail = new Mail(); 24 | mail.setFrom(FROM); 25 | mail.setHost(HOST); 26 | mail.setPort(PORT); 27 | 28 | Properties expectedProperties = new Properties(); 29 | expectedProperties.put("mail.smtp.from", mail.getFrom()); 30 | expectedProperties.put("mail.smtp.host", mail.getHost()); 31 | expectedProperties.put("mail.smtp.port", mail.getPort()); 32 | expectedProperties.put("mail.smtp.auth", "true"); 33 | 34 | assertEquals(expectedProperties, new MailProperties(mail).create()); 35 | } 36 | 37 | @ParameterizedTest 38 | @CsvSource({ 39 | "SSL, ssl", 40 | "STARTTLS, starttls" 41 | }) 42 | void shouldCreateMailPropertiesWithSecurityProtocolSpecified(SecurityProtocol securityProtocol, 43 | String propertyNamePart) { 44 | 45 | Mail mail = new Mail(); 46 | mail.setFrom(FROM); 47 | mail.setHost(HOST); 48 | mail.setPort(PORT); 49 | mail.setSecurityProtocol(securityProtocol); 50 | 51 | Properties expectedProperties = new Properties(); 52 | expectedProperties.put("mail.smtp.from", mail.getFrom()); 53 | expectedProperties.put("mail.smtp.host", mail.getHost()); 54 | expectedProperties.put("mail.smtp.port", mail.getPort()); 55 | expectedProperties.put("mail.smtp.auth", "true"); 56 | expectedProperties.put("mail.smtp." + propertyNamePart + ".enable", true); 57 | 58 | assertEquals(expectedProperties, new MailProperties(mail).create()); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /allure-notifications-api/src/test/java/guru/qa/allure/notifications/util/MailUtilTests.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.util; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | import java.util.Arrays; 8 | 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.params.ParameterizedTest; 11 | import org.junit.jupiter.params.provider.CsvSource; 12 | 13 | import guru.qa.allure.notifications.exceptions.InvalidArgumentException; 14 | import jakarta.mail.internet.InternetAddress; 15 | 16 | class MailUtilTests { 17 | @ParameterizedTest() 18 | @CsvSource(delimiter = ';', value = { 19 | "test@gmail.com; 1", 20 | "test1@gmail.com,test2@gmail.com; 2", 21 | "test1@gmail.com, test2@gmail.com; 2" 22 | }) 23 | void shouldParseRecipients(String recipients, int numberOfMails) { 24 | InternetAddress[] addresses = MailUtil.recipients(recipients); 25 | assertEquals(addresses.length, numberOfMails); 26 | assertTrue(Arrays.stream(addresses).allMatch(a -> recipients.contains(a.getAddress()))); 27 | } 28 | 29 | @Test 30 | void shouldThrowExceptionIfPassedInvalidEmail() { 31 | String httpsMail = "https://test@gmail.com"; 32 | Exception exception = assertThrows(InvalidArgumentException.class, () -> MailUtil.recipients(httpsMail)); 33 | assertEquals("Invalid email address: " + httpsMail, exception.getMessage()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /allure-notifications-api/src/test/java/guru/qa/allure/notifications/util/ProxyManagerTest.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications.util; 2 | 3 | import guru.qa.allure.notifications.config.proxy.Proxy; 4 | import kong.unirest.Config; 5 | import kong.unirest.Unirest; 6 | 7 | import org.junit.jupiter.api.Disabled; 8 | import org.junit.jupiter.params.ParameterizedTest; 9 | import org.junit.jupiter.params.provider.Arguments; 10 | import org.junit.jupiter.params.provider.MethodSource; 11 | import org.mockito.Mockito; 12 | 13 | import java.util.UUID; 14 | import java.util.stream.Stream; 15 | 16 | @Disabled 17 | class ProxyManagerTest { 18 | 19 | public static Stream proxySource() { 20 | return Stream.of( 21 | Arguments.of( 22 | "proxy is null", 23 | null, 24 | 0, 25 | 0 26 | ), 27 | Arguments.of( 28 | "username is null", 29 | createProxyConfig( 30 | UUID.randomUUID().toString(), 31 | 443, 32 | null, 33 | UUID.randomUUID().toString() 34 | ), 35 | 1, 36 | 0 37 | ), 38 | Arguments.of( 39 | "password is null", 40 | createProxyConfig( 41 | UUID.randomUUID().toString(), 42 | 443, 43 | UUID.randomUUID().toString(), 44 | null 45 | ), 46 | 1, 47 | 0 48 | ), 49 | Arguments.of( 50 | "is proxy", 51 | createProxyConfig( 52 | UUID.randomUUID().toString(), 53 | 443, 54 | UUID.randomUUID().toString(), 55 | UUID.randomUUID().toString() 56 | ), 57 | 0, 58 | 1 59 | ) 60 | ); 61 | } 62 | 63 | private static Proxy createProxyConfig(String host, Integer port, String username, String password) { 64 | Proxy proxy = new Proxy(); 65 | proxy.setHost(host); 66 | proxy.setPort(port); 67 | proxy.setUsername(username); 68 | proxy.setPassword(password); 69 | return proxy; 70 | } 71 | 72 | @ParameterizedTest(name = "ProxyManager: {0}") 73 | @MethodSource("proxySource") 74 | void manageProxyProxyIsNullTest(String condition, Proxy proxy, Integer notProxyTimes, Integer withProxyTimes) { 75 | Config config = Mockito.mock(Config.class); 76 | Unirest unirest = Mockito.mock(Unirest.class); 77 | ProxyManager.manageProxy(proxy); 78 | // Mockito.verify(unirest, Mockito.times(1)); 79 | 80 | Mockito.verify(config, Mockito.times(withProxyTimes)) 81 | .proxy(Mockito.anyString(), Mockito.anyInt(), Mockito.anyString(), Mockito.anyString()); 82 | Mockito.verify(config, Mockito.times(notProxyTimes)) 83 | .proxy(Mockito.anyString(), Mockito.anyInt()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /allure-notifications-api/src/test/resources/data/testConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": { 3 | "project": "TEST PROJECT", 4 | "environment": "Test", 5 | "comment": "Test comment", 6 | "reportLink": "https://www.example.com", 7 | "language": "en", 8 | "allureFolder": "build/allure-report/", 9 | "enableChart": true, 10 | "logo": "logo.png", 11 | "durationFormat": "HH:mm:ss.SSS", 12 | "enableSuitesPublishing": true, 13 | "customData": { 14 | "variable1": "value1", 15 | "variable2": "value2" 16 | } 17 | }, 18 | "mail": { 19 | "host": "smtp.gmail.com", 20 | "port": "465", 21 | "username": "user", 22 | "password": "password", 23 | "securityProtocol": "SSL", 24 | "from": "test1@gmail.com", 25 | "recipient": "test2@gmail.com" 26 | }, 27 | "rocketChat" : { 28 | "url": "https://www.example.com", 29 | "auth_token": "authToken", 30 | "user_id": "userId", 31 | "channel": "channel", 32 | "templatePath": "/templates/rocket.ftl" 33 | }, 34 | "discord": { 35 | "botToken": "testToken", 36 | "channelId": "testChannelId", 37 | "templatePath": "/templates/markdown.ftl" 38 | }, 39 | "telegram": { 40 | "token": "testToken", 41 | "chat": "testChat", 42 | "topic": "testTopic", 43 | "replyTo": "user", 44 | "templatePath": "/templates/telegram.ftl" 45 | }, 46 | "proxy": { 47 | "host": "localhost", 48 | "port": 80, 49 | "username": "bob", 50 | "password": "p@$$" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /allure-notifications-api/src/test/resources/data/testConfig_noData.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": { 3 | }, 4 | "mail": { 5 | }, 6 | "rocketChat": { 7 | }, 8 | "discord": { 9 | }, 10 | "telegram": { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /allure-notifications-api/src/test/resources/data/testSuites.json: -------------------------------------------------------------------------------- 1 | {"total":2,"items":[{"uid":"87b8be25e9c9e007e80824e508a2043b","name":"batch-1","statistic":{"failed":1,"broken":0,"skipped":0,"passed":1,"unknown":0,"total":2}},{"uid":"9c1ca7280386cbcc24a7a39c461dd533","name":"batch-2","statistic":{"failed":0,"broken":0,"skipped":0,"passed":1,"unknown":0,"total":1}}]} -------------------------------------------------------------------------------- /allure-notifications-api/src/test/resources/data/testSummary.json: -------------------------------------------------------------------------------- 1 | {"reportName":"TEST Report","testRuns":[],"statistic":{"failed":1,"broken":0,"skipped":0,"passed":2,"unknown":0,"total":3},"time":{"start":1723022106448,"stop":1723022130459,"duration":24011,"minDuration":2045,"maxDuration":15512,"sumDuration":21736}} -------------------------------------------------------------------------------- /allure-notifications-api/src/test/resources/messages/customHtml.txt: -------------------------------------------------------------------------------- 1 |

    Results:

    2 | Environment: Test
    3 | Comment: Test comment
    4 | Duration: 00:00:24.011
    5 | Total scenarios: 3 6 |
      7 |
    • Total passed: 2 (66.7 %)
    • 8 |
    • Total failed: 1 (33.3 %)
    • 9 |
    10 | Report available at the link: https://www.example.com 11 |
    12 |

    Custom section:

    13 | Custom data 1: value1
    14 | Custom data 2: value2 -------------------------------------------------------------------------------- /allure-notifications-api/src/test/resources/messages/html.txt: -------------------------------------------------------------------------------- 1 |

    Results:

    2 | Environment: Test
    3 | Comment: Test comment
    4 | Duration: 00:00:24.011
    5 | Total scenarios: 3 6 |
      7 |
    • Total passed: 2 (66.7 %)
    • 8 |
    • Total failed: 1 (33.3 %)
    • 9 |
    10 | Number of test suites: 2
    11 | 12 | 13 | 19 | 20 |
    14 | Name of the test suite: batch-1
    15 | Total scenarios: 2
    16 |
  • Total passed: 1
  • 17 |
  • Total failed: 1
  • 18 |

    21 | 22 | 23 | 28 | 29 |
    24 | Name of the test suite: batch-2
    25 | Total scenarios: 1
    26 |
  • Total passed: 1
  • 27 |

    30 | Report available at the link: https://www.example.com -------------------------------------------------------------------------------- /allure-notifications-api/src/test/resources/messages/markdown.txt: -------------------------------------------------------------------------------- 1 | *Results:* 2 | *Environment:* Test 3 | *Comment:* Test comment 4 | *Duration:* 00:00:24.011 5 | *Total scenarios:* 3 6 | *Total passed:* 2 (66.7 %) 7 | *Total failed:* 1 (33.3 %) 8 | *Number of test suites:* 2 9 | *Name of the test suite:* batch-1 10 | > *Total scenarios:* 2 11 | > *Total passed:* 1 12 | > *Total failed:* 1 13 | *Name of the test suite:* batch-2 14 | > *Total scenarios:* 1 15 | > *Total passed:* 1 16 | *Report available at the link:* https://www.example.com -------------------------------------------------------------------------------- /allure-notifications-api/src/test/resources/messages/rocket.txt: -------------------------------------------------------------------------------- 1 | **Results:** 2 | **-Environment:** Test 3 | **-Comment:** Test comment 4 | **-Duration:** **00:00:24.011** 5 | **-Total scenarios:** 3 6 | **-Total passed:** 2 **(66.7 %)** 7 | **-Total failed:** 1 **(33.3 %)** 8 | **-Number of test suites:** **2** 9 | **-Name of the test suite:** **batch-1** 10 | **-Total scenarios:** **2** 11 | **-Total passed:** 1 12 | **-Total failed:** 1 13 | **-Name of the test suite:** **batch-2** 14 | **-Total scenarios:** **1** 15 | **-Total passed:** 1 16 | **Report available at the link:** https://www.example.com -------------------------------------------------------------------------------- /allure-notifications-api/src/test/resources/messages/telegram.txt: -------------------------------------------------------------------------------- 1 | Results: 2 | Environment: Test 3 | Comment: Test comment 4 | Duration: 00:00:24.011 5 | Total scenarios: 3 6 | Total passed: 2 (66.7 %) 7 | Total failed: 1 (33.3 %) 8 | Number of test suites: 2 9 | Name of the test suite: batch-1 10 | Total scenarios: 2 11 | Total passed: 1 12 | Total failed: 1 13 | Name of the test suite: batch-2 14 | Total scenarios: 1 15 | Total passed: 1 16 | Report available at the link: https://www.example.com -------------------------------------------------------------------------------- /allure-notifications-api/src/test/resources/template/emptyTemplate.ftl: -------------------------------------------------------------------------------- 1 | for test purposes -------------------------------------------------------------------------------- /allure-notifications-api/src/test/resources/template/testTemplateAsFile.ftl: -------------------------------------------------------------------------------- 1 | <#import "utils.ftl" as utils> 2 | <#compress> 3 |

    ${phrases.results}:

    4 | ${phrases.environment}: ${environment}
    5 | ${phrases.comment}: ${comment}
    6 | ${phrases.scenario.duration}: ${time}
    7 | ${phrases.scenario.totalScenarios}: ${statistic.total} 8 |
      9 | <#if statistic.passed != 0 > 10 |
    • ${phrases.scenario.totalPassed}: ${statistic.passed} <@utils.printPercentage input=statistic.passed total=statistic.total />
    • 11 | 12 | <#if statistic.failed != 0 > 13 |
    • ${phrases.scenario.totalFailed}: ${statistic.failed} <@utils.printPercentage input=statistic.failed total=statistic.total />
    • 14 | 15 | <#if statistic.broken != 0 > 16 |
    • ${phrases.scenario.totalBroken}: ${statistic.broken}
    • 17 | 18 | <#if statistic.unknown != 0 > 19 |
    • ${phrases.scenario.totalUnknown}: ${statistic.unknown}
    • 20 | 21 | <#if statistic.skipped != 0 > 22 |
    • ${phrases.scenario.totalSkipped}: ${statistic.skipped}
    • 23 | 24 |
    25 | <#if reportLink??>${phrases.reportAvailableAtLink}: ${reportLink} 26 |
    27 |

    Custom section:

    28 | Custom data 1: ${customData.variable1}
    29 | Custom data 2: ${customData.variable2} 30 | -------------------------------------------------------------------------------- /allure-notifications-api/src/test/resources/template/testTemplateAsResource.ftl: -------------------------------------------------------------------------------- 1 | <#import "/templates/utils.ftl" as utils> 2 | <#compress> 3 |

    ${phrases.results}:

    4 | ${phrases.environment}: ${environment}
    5 | ${phrases.comment}: ${comment}
    6 | ${phrases.scenario.duration}: ${time}
    7 | ${phrases.scenario.totalScenarios}: ${statistic.total} 8 |
      9 | <#if statistic.passed != 0 > 10 |
    • ${phrases.scenario.totalPassed}: ${statistic.passed} <@utils.printPercentage input=statistic.passed total=statistic.total />
    • 11 | 12 | <#if statistic.failed != 0 > 13 |
    • ${phrases.scenario.totalFailed}: ${statistic.failed} <@utils.printPercentage input=statistic.failed total=statistic.total />
    • 14 | 15 | <#if statistic.broken != 0 > 16 |
    • ${phrases.scenario.totalBroken}: ${statistic.broken}
    • 17 | 18 | <#if statistic.unknown != 0 > 19 |
    • ${phrases.scenario.totalUnknown}: ${statistic.unknown}
    • 20 | 21 | <#if statistic.skipped != 0 > 22 |
    • ${phrases.scenario.totalSkipped}: ${statistic.skipped}
    • 23 | 24 |
    25 | <#if reportLink??>${phrases.reportAvailableAtLink}: ${reportLink} 26 |
    27 |

    Custom section:

    28 | Custom data 1: ${customData.variable1}
    29 | Custom data 2: ${customData.variable2} 30 | -------------------------------------------------------------------------------- /allure-notifications-api/src/test/resources/template/utils.ftl: -------------------------------------------------------------------------------- 1 | 2 | <#macro printPercentage input total> 3 | (${( (input * 100.00) / total )?string("##.#")} %)<#t> 4 | 5 | -------------------------------------------------------------------------------- /allure-notifications/build.gradle: -------------------------------------------------------------------------------- 1 | project.description = 'Library for sending notifications about autotest results (to generate executable Fat JAR)' 2 | 3 | dependencies { 4 | annotationProcessor(group: 'org.projectlombok', name: 'lombok', version: lombokVersion) 5 | compileOnly(group: 'org.projectlombok', name: 'lombok', version: lombokVersion) 6 | 7 | implementation project(':allure-notifications-api') 8 | 9 | implementation platform(group: 'org.slf4j', name: 'slf4j-bom', version: slf4jVersion) 10 | implementation('org.slf4j:jul-to-slf4j') // Java Util Logging -> SLF4J 11 | implementation('org.slf4j:jcl-over-slf4j') // commons-logging -> SLF4J 12 | 13 | implementation platform(group: 'org.apache.logging.log4j', name: 'log4j-bom', version: '2.24.3') 14 | implementation(group: 'org.apache.logging.log4j', name: 'log4j-api') 15 | implementation(group: 'org.apache.logging.log4j', name: 'log4j-core') 16 | implementation(group: 'org.apache.logging.log4j', name: 'log4j-slf4j2-impl') 17 | } 18 | 19 | configurations.all { 20 | exclude module: 'commons-logging' // Use jcl-over-slf4j bridge instead 21 | } 22 | 23 | jar { 24 | dependsOn(configurations.runtimeClasspath) 25 | manifest { 26 | attributes( 27 | 'Main-Class': 'guru.qa.allure.notifications.Application' 28 | ) 29 | } 30 | from { 31 | configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } 32 | } 33 | duplicatesStrategy = DuplicatesStrategy.WARN 34 | } 35 | -------------------------------------------------------------------------------- /allure-notifications/src/main/java/guru/qa/allure/notifications/Application.java: -------------------------------------------------------------------------------- 1 | package guru.qa.allure.notifications; 2 | 3 | import java.io.IOException; 4 | 5 | import guru.qa.allure.notifications.clients.Notification; 6 | import guru.qa.allure.notifications.config.ApplicationConfig; 7 | import guru.qa.allure.notifications.config.Config; 8 | import guru.qa.allure.notifications.util.LogInterceptor; 9 | import guru.qa.allure.notifications.util.ProxyManager; 10 | import kong.unirest.Unirest; 11 | import lombok.extern.slf4j.Slf4j; 12 | 13 | @Slf4j 14 | public class Application { 15 | 16 | public static void main(String[] args) throws IOException { 17 | log.info("Start..."); 18 | Config config = new ApplicationConfig().readConfig(); 19 | 20 | Unirest.config() 21 | .interceptor(new LogInterceptor()); 22 | 23 | ProxyManager.manageProxy(config.getProxy()); 24 | 25 | boolean successfulSending; 26 | try { 27 | successfulSending = Notification.send(config); 28 | } catch (Exception e) { 29 | log.error(e.getMessage(), e); 30 | successfulSending = false; 31 | } finally { 32 | Unirest.shutDown(); 33 | } 34 | 35 | if (!successfulSending) { 36 | System.exit(1); 37 | } 38 | 39 | log.info("Finish."); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /allure-notifications/src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | group = 'allure.notifications' 3 | version = '4.9.0' 4 | } 5 | 6 | subprojects { 7 | apply plugin: 'java-library' 8 | 9 | java { 10 | sourceCompatibility = 1.8 11 | } 12 | 13 | tasks.withType(JavaCompile) { 14 | options.encoding = 'UTF-8' 15 | } 16 | 17 | // Checkstyle requires Java 11 starting from 10.0 18 | if (JavaVersion.current().isJava11Compatible()) { 19 | apply plugin: 'checkstyle' 20 | 21 | checkstyle { 22 | toolVersion = '10.21.4' 23 | ignoreFailures = false 24 | showViolations = true 25 | } 26 | 27 | tasks.withType(Checkstyle) { 28 | reports { 29 | xml.required = false 30 | html.required = false 31 | } 32 | } 33 | } 34 | } 35 | 36 | ext { 37 | lombokVersion = '1.18.38' 38 | slf4jVersion = '2.0.16' 39 | unirestVersion = '3.14.5' 40 | } 41 | -------------------------------------------------------------------------------- /config/checkstyle/checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 51 | 54 | 55 | 56 | 57 | 58 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 69 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 114 | 115 | 116 | 117 | 119 | 120 | 121 | 122 | 124 | 125 | 126 | 127 | 129 | 130 | 131 | 132 | 134 | 135 | 136 | 137 | 138 | 140 | 141 | 142 | 143 | 145 | 146 | 147 | 148 | 150 | 151 | 152 | 153 | 155 | 156 | 157 | 158 | 160 | 162 | 164 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 183 | 184 | 185 | 186 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 203 | 204 | 205 | 206 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/allure-notifications/1495a73072ed767a8ca9e8914048c88748eedc96/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH="\\\"\\\"" 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "changelog": { 3 | "labels": { 4 | "other": "New Feature", 5 | "fix": "Bug Fix", 6 | "refactor": "Refactoring" 7 | }, 8 | "repo": "username/repo" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /readme_images/email_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/allure-notifications/1495a73072ed767a8ca9e8914048c88748eedc96/readme_images/email_en.png -------------------------------------------------------------------------------- /readme_images/jenkins_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/allure-notifications/1495a73072ed767a8ca9e8914048c88748eedc96/readme_images/jenkins_config.png -------------------------------------------------------------------------------- /readme_images/mattermost-ru.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/allure-notifications/1495a73072ed767a8ca9e8914048c88748eedc96/readme_images/mattermost-ru.png -------------------------------------------------------------------------------- /readme_images/slack-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/allure-notifications/1495a73072ed767a8ca9e8914048c88748eedc96/readme_images/slack-en.png -------------------------------------------------------------------------------- /readme_images/slack_guide/Screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/allure-notifications/1495a73072ed767a8ca9e8914048c88748eedc96/readme_images/slack_guide/Screenshot_1.png -------------------------------------------------------------------------------- /readme_images/slack_guide/Screenshot_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/allure-notifications/1495a73072ed767a8ca9e8914048c88748eedc96/readme_images/slack_guide/Screenshot_10.png -------------------------------------------------------------------------------- /readme_images/slack_guide/Screenshot_11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/allure-notifications/1495a73072ed767a8ca9e8914048c88748eedc96/readme_images/slack_guide/Screenshot_11.png -------------------------------------------------------------------------------- /readme_images/slack_guide/Screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/allure-notifications/1495a73072ed767a8ca9e8914048c88748eedc96/readme_images/slack_guide/Screenshot_2.png -------------------------------------------------------------------------------- /readme_images/slack_guide/Screenshot_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/allure-notifications/1495a73072ed767a8ca9e8914048c88748eedc96/readme_images/slack_guide/Screenshot_3.png -------------------------------------------------------------------------------- /readme_images/slack_guide/Screenshot_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/allure-notifications/1495a73072ed767a8ca9e8914048c88748eedc96/readme_images/slack_guide/Screenshot_4.png -------------------------------------------------------------------------------- /readme_images/slack_guide/Screenshot_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/allure-notifications/1495a73072ed767a8ca9e8914048c88748eedc96/readme_images/slack_guide/Screenshot_5.png -------------------------------------------------------------------------------- /readme_images/slack_guide/Screenshot_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/allure-notifications/1495a73072ed767a8ca9e8914048c88748eedc96/readme_images/slack_guide/Screenshot_6.png -------------------------------------------------------------------------------- /readme_images/slack_guide/Screenshot_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/allure-notifications/1495a73072ed767a8ca9e8914048c88748eedc96/readme_images/slack_guide/Screenshot_7.png -------------------------------------------------------------------------------- /readme_images/slack_guide/Screenshot_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/allure-notifications/1495a73072ed767a8ca9e8914048c88748eedc96/readme_images/slack_guide/Screenshot_8.png -------------------------------------------------------------------------------- /readme_images/slack_guide/Screenshot_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/allure-notifications/1495a73072ed767a8ca9e8914048c88748eedc96/readme_images/slack_guide/Screenshot_9.png -------------------------------------------------------------------------------- /readme_images/slack_guide/jenkins_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/allure-notifications/1495a73072ed767a8ca9e8914048c88748eedc96/readme_images/slack_guide/jenkins_config.png -------------------------------------------------------------------------------- /readme_images/telegram-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/allure-notifications/1495a73072ed767a8ca9e8914048c88748eedc96/readme_images/telegram-en.png -------------------------------------------------------------------------------- /readme_images/telegram_ru.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/allure-notifications/1495a73072ed767a8ca9e8914048c88748eedc96/readme_images/telegram_ru.png -------------------------------------------------------------------------------- /readme_images/telegram_ua.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/allure-notifications/1495a73072ed767a8ca9e8914048c88748eedc96/readme_images/telegram_ua.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include 'allure-notifications-api' 2 | include 'allure-notifications' 3 | 4 | dependencyResolutionManagement { 5 | repositories { 6 | mavenCentral() 7 | } 8 | } 9 | --------------------------------------------------------------------------------