From 9eac4bb6ee52392c1d3ce65fe4979021a6c9429b Mon Sep 17 00:00:00 2001 From: rconner46 Date: Mon, 20 Nov 2023 12:13:26 -0600 Subject: [PATCH] Initialize Repo --- .gitignore | 19 + .java-version | 1 + LICENSE | 202 +++++++ README.md | 61 +- build-tools/eclipse-formatter-config.xml | 407 ++++++++++++++ pom.xml | 223 ++++++++ .../auto/testrail/client/TestRailApi.java | 195 +++++++ .../auto/testrail/client/TestRailClient.java | 525 ++++++++++++++++++ .../client/TestRailClientFactory.java | 76 +++ .../client/TestRailParamValidator.java | 210 +++++++ .../testrail/client/TestRailResultLogger.java | 273 +++++++++ .../client/TestRailResultUploader.java | 254 +++++++++ .../auto/testrail/client/TestRailUtil.java | 177 ++++++ .../client/enums/TestResultStatus.java | 55 ++ .../client/errors/TestRailErrorStatus.java | 59 ++ .../client/errors/TestRailException.java | 86 +++ .../interceptors/GenericErrorInterceptor.java | 76 +++ .../interceptors/HeadersInterceptor.java | 57 ++ .../client/interceptors/UrlInterceptor.java | 52 ++ .../models/applause/AcquireTestPlanDto.java | 31 ++ .../client/models/config/TestRailConfig.java | 46 ++ .../models/config/TestRailConfigExtended.java | 31 ++ .../models/config/TestRailCredentials.java | 30 + .../client/models/config/TestRailDto.java | 42 ++ .../models/config/TestRailStatusMaps.java | 112 ++++ .../internal/TestRailRunsAndInvalidCases.java | 33 ++ .../internal/TestRailStatusComment.java | 23 + .../internal/TestRailValidateRequest.java | 28 + .../client/models/testrail/AddPlanDto.java | 41 ++ .../models/testrail/AddPlanEntryDto.java | 60 ++ .../testrail/AddTestResultForCaseDto.java | 49 ++ .../testrail/AddTestResultsForCaseDto.java | 39 ++ .../client/models/testrail/CustomStepDto.java | 26 + .../models/testrail/PaginatedBulkCaseDto.java | 32 ++ .../models/testrail/PaginatedBulkPlanDto.java | 32 ++ .../models/testrail/PaginatedBulkTestDto.java | 32 ++ .../models/testrail/PaginatedLinkDto.java | 26 + .../client/models/testrail/PlanDto.java | 109 ++++ .../client/models/testrail/PlanEntryDto.java | 40 ++ .../models/testrail/PlanEntryRunDto.java | 38 ++ .../client/models/testrail/ProjectDto.java | 47 ++ .../client/models/testrail/StatusDto.java | 58 ++ .../client/models/testrail/TestCaseDto.java | 29 + .../client/models/testrail/TestDto.java | 84 +++ .../client/models/testrail/TestResultDto.java | 59 ++ .../client/models/testrail/TestRunDto.java | 97 ++++ .../client/models/testrail/TestSuiteDto.java | 52 ++ .../models/testrail/UpdatePlanEntryDto.java | 46 ++ .../models/testrail/UpdateTestResultDto.java | 47 ++ .../testrail/client/TestRailClientTest.java | 428 ++++++++++++++ .../client/TestRailParamValidatorTest.java | 185 ++++++ .../testrail/client/TestRailUtilTest.java | 102 ++++ .../org.mockito.plugins.MockMaker | 1 + 53 files changed, 5142 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 .java-version create mode 100644 LICENSE create mode 100644 build-tools/eclipse-formatter-config.xml create mode 100644 pom.xml create mode 100644 src/main/java/com/applause/auto/testrail/client/TestRailApi.java create mode 100644 src/main/java/com/applause/auto/testrail/client/TestRailClient.java create mode 100644 src/main/java/com/applause/auto/testrail/client/TestRailClientFactory.java create mode 100644 src/main/java/com/applause/auto/testrail/client/TestRailParamValidator.java create mode 100644 src/main/java/com/applause/auto/testrail/client/TestRailResultLogger.java create mode 100644 src/main/java/com/applause/auto/testrail/client/TestRailResultUploader.java create mode 100644 src/main/java/com/applause/auto/testrail/client/TestRailUtil.java create mode 100644 src/main/java/com/applause/auto/testrail/client/enums/TestResultStatus.java create mode 100644 src/main/java/com/applause/auto/testrail/client/errors/TestRailErrorStatus.java create mode 100644 src/main/java/com/applause/auto/testrail/client/errors/TestRailException.java create mode 100644 src/main/java/com/applause/auto/testrail/client/interceptors/GenericErrorInterceptor.java create mode 100644 src/main/java/com/applause/auto/testrail/client/interceptors/HeadersInterceptor.java create mode 100644 src/main/java/com/applause/auto/testrail/client/interceptors/UrlInterceptor.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/applause/AcquireTestPlanDto.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/config/TestRailConfig.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/config/TestRailConfigExtended.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/config/TestRailCredentials.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/config/TestRailDto.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/config/TestRailStatusMaps.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/internal/TestRailRunsAndInvalidCases.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/internal/TestRailStatusComment.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/internal/TestRailValidateRequest.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/testrail/AddPlanDto.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/testrail/AddPlanEntryDto.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/testrail/AddTestResultForCaseDto.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/testrail/AddTestResultsForCaseDto.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/testrail/CustomStepDto.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/testrail/PaginatedBulkCaseDto.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/testrail/PaginatedBulkPlanDto.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/testrail/PaginatedBulkTestDto.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/testrail/PaginatedLinkDto.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/testrail/PlanDto.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/testrail/PlanEntryDto.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/testrail/PlanEntryRunDto.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/testrail/ProjectDto.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/testrail/StatusDto.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/testrail/TestCaseDto.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/testrail/TestDto.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/testrail/TestResultDto.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/testrail/TestRunDto.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/testrail/TestSuiteDto.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/testrail/UpdatePlanEntryDto.java create mode 100644 src/main/java/com/applause/auto/testrail/client/models/testrail/UpdateTestResultDto.java create mode 100644 src/test/java/com/applause/auto/testrail/client/TestRailClientTest.java create mode 100644 src/test/java/com/applause/auto/testrail/client/TestRailParamValidatorTest.java create mode 100644 src/test/java/com/applause/auto/testrail/client/TestRailUtilTest.java create mode 100644 src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa14b81 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +.settings +build/ +out/ +target/ +bin/ +.gradle/ +.classpath +.spotless_index +.project +.idea/ +*.iml +*.ipr +*.iws +*.jar +.vscode/ +.*.swp +.*.swo + +**/.DS_Store diff --git a/.java-version b/.java-version new file mode 100644 index 0000000..98d9bcb --- /dev/null +++ b/.java-version @@ -0,0 +1 @@ +17 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md index 56ce26e..ae4194a 100644 --- a/README.md +++ b/README.md @@ -1 +1,60 @@ -# applause-testrail-client \ No newline at end of file +## Testrail client library +Testrail client library for Applause Automation. + +#### Installation +This library is not built or deployed on its own, it must be built with another package. + +#### Linter/Formatter: +The linter/formatter will run automatically during a build, but builds do not happen automatically. You must manually create a build or run unit tests to trigger auto-formatter. + +This project uses the same formatter rules as auto-api. + +Please run `mvn test` to run unit tests and trigger the auto-formatter before committing code. + +#### Unit Tests +`mvn test` + +#### Example usage: +```java +// External HTTPClient needs to be passed in +OkHttpClient httpClient = new OkHttpClient(); + +// configuration object(s) +TestRailConfig testRailConfig = + TestRailConfig.builder() + .email("email") + .apiKey("apiKey") + .url("testrailUrl") + .build(); + +// TestRailClient can create 4 types of clients + +// 1a) Using TestRail API Client +final var baseApi = new TestRailClientFactory(httpClient).getTestRailApi(testRailConfig); +ProjectDto projectDto1 = baseApi.getProject(8L).join().body(); +System.out.println(projectDto1); + +// 2) Using TestRailClient, which wraps the raw TestRail API Client +final var testRailClient = + new TestRailClientFactory(httpClient).getTestRailClient(testRailConfig); +ProjectDto projectDto3 = testRailClient.getProject(8L); +System.out.println(projectDto3); + +// 3) Using ParamValidator, which uses the TestRailClient for basic param validation +final var testRailParamvalidator = new TestRailParamValidator(testRailClient); +ProjectDto projectDto4 = testRailParamvalidator.validateTestRailProjectId(8L); +System.out.println(projectDto4); + +// 4) Using Specialized lazy/builder Reporting Client +final var testRailResultLogger = new TestRailResultLogger(testRailClient); +Table resultsToLog = HashBasedTable.create(); +resultsToLog.put( + "Run Name to Use", + 0L, // Project ID + new TestRailStatusComment(TestRailUtil.TESTRAIL_PASSED_STATUS_ID, "Result comment")); +final var invalidCaseIds = + testRailResultLogger.execute( + // Project ID, Suite ID, Plan Name, Plan ID + resultsToLog, new TestRailValidateRequest(0L, 0L, "planName", null, false)); +System.out.println(invalidCaseIds); +``` diff --git a/build-tools/eclipse-formatter-config.xml b/build-tools/eclipse-formatter-config.xml new file mode 100644 index 0000000..9fcb382 --- /dev/null +++ b/build-tools/eclipse-formatter-config.xml @@ -0,0 +1,407 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..07276e0 --- /dev/null +++ b/pom.xml @@ -0,0 +1,223 @@ + + + + 4.0.0 + com.applause + applause-testrail-client + 6.0.0 + jar + applause-testrail-client + + + + s3.applause-public-repo + s3://prod-repo.applause.com/repository/public + + + s3.applause-public-snapshots + s3://prod-repo.applause.com/repository/snapshots + + + + + 17 + 17 + 2.10.1 + 32.1.3-jre + 4.8.0 + 4.12.0 + 2.9.0 + 3.1.0 + 5.10.0 + 1.18.30 + 2.0.9 + 5.6.0 + 3.13.0 + + + + + org.junit.jupiter + junit-jupiter + ${org.junit.jupiter.version} + test + + + com.google.code.gson + gson + ${com.google.code.gson.version} + + + com.google.guava + guava + ${com.google.code.guava.version} + + + com.squareup.retrofit2 + retrofit + ${com.squareup.retrofit2.version} + + + com.squareup.retrofit2 + converter-gson + ${com.squareup.retrofit2.version} + + + com.squareup.retrofit2 + converter-scalars + ${com.squareup.retrofit2.version} + + + org.projectlombok + lombok + ${org.projectlombok.version} + + + jakarta.ws.rs + jakarta.ws.rs-api + ${jakarta.ws.rs.version} + + + com.squareup.okhttp3 + okhttp + ${com.squareup.okhttp3.version} + + + org.apache.commons + commons-lang3 + ${apachecommons.version} + + + com.github.spotbugs + spotbugs-annotations + ${com.github.spotbugs.version} + compile + + + org.slf4j + slf4j-api + ${org.slf4j.version} + + + org.mockito + mockito-core + ${org.mockito.version} + test + + + org.mockito + mockito-junit-jupiter + ${org.mockito.version} + test + + + + + + + maven-assembly-plugin + + + + App.main + + + + jar-with-dependencies + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + + com.diffplug.spotless + spotless-maven-plugin + 2.40.0 + + + true + ${project.basedir}/.spotless_index + + + + + + + 1.18.1 + + + + /* +* +* Copyright © $YEAR Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ + + + + + + + pom.xml + + + + + + + + + apply + + validate + + + + + + + com.github.seahen + maven-s3-wagon + 1.3.3 + + + + diff --git a/src/main/java/com/applause/auto/testrail/client/TestRailApi.java b/src/main/java/com/applause/auto/testrail/client/TestRailApi.java new file mode 100644 index 0000000..046bdc0 --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/TestRailApi.java @@ -0,0 +1,195 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client; + +import com.applause.auto.testrail.client.models.testrail.*; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import retrofit2.Response; +import retrofit2.http.*; + +/** Retrofit client for interacting with the TestRail API */ +public interface TestRailApi { + + /** + * Updates an existing test plan (partial updates are supported, i.e. you can submit and update + * specific fields only). If you want to update only certain fields, leave others null + * + * @param planId The ID of the test plan + * @param entryId The ID of the test plan entry (note: not the test run ID) + * @param updatePlanEntryDto info about the plan entry to update + * @return If successful, this method returns the updated test plan entry including test runs + * using the same response format as the entries field of get_plan, but for a single entry + * instead of a list of entries. + */ + @POST("/update_plan_entry/{plan_id}/{entry_id}") + CompletableFuture> updatePlanEntry( + @Path("plan_id") long planId, + @Path("entry_id") final String entryId, + @Body final UpdatePlanEntryDto updatePlanEntryDto); + + /** + * Adds one or more new test runs to a test plan. + * + * @param planId The ID of the plan the test runs should be added to + * @param addPlanEntryDto info about the plan entry to add + * @return If successful, this method returns the updated test plan entry including test runs + * using the same response format as the entries field of get_plan, but for a single entry + * instead of a list of entries. + */ + @POST("/add_plan_entry/{plan_id}") + CompletableFuture> addPlanEntry( + @Path("plan_id") long planId, final @Body AddPlanEntryDto addPlanEntryDto); + + /** + * Returns an existing test plan. + * + * @param planId The ID of the test plan + * @return Test plan. The entries field includes an array of test plan entries. A test plan entry + * is a group of test runs that belong to the same test suite (just like in the user + * interface). Each group can have a variable amount of test runs and also supports + * configurations. Please also see add_plan and add_plan_entry. + */ + @GET("/get_plan/{plan_id}") + CompletableFuture> getPlan(@Path("plan_id") long planId); + + /** + * Returns all existing test plans in a project. + * + * @param projectId The ID of the project + * @param offset number of items to offset the pagination by ie : an offset of 250 will skip the + * first 250 items + * @param limit max number of items to return + * @return A list of test plans for the project. This can be used to find an existing plan id that + * was created manually. + */ + @GET("/get_plans/{project_id}") + CompletableFuture> getPlansForProject( + @Path("project_id") long projectId, @Query("offset") int offset, @Query("limit") int limit); + + /** + * Returns an existing test plan. + * + * @param projectId The ID of the project the test plan should be added to + * @param addPlanDto data fields to set in new plan + * @return Test plan. The entries field includes an array of test plan entries. A test plan entry + * is a group of test runs that belong to the same test suite (just like in the user + * interface). Each group can have a variable amount of test runs and also supports + * configurations. Please also see add_plan and add_plan_entry. + */ + @POST("/add_plan/{project_id}") + CompletableFuture> addPlan( + @Path("project_id") long projectId, final @Body AddPlanDto addPlanDto); + + /** + * Adds a new test result, comment or assigns a test. It's recommended to use add_results instead + * if you plan to add results for multiple tests. + * + *

NOTE: This doesn't currently support updating custom fields since there's no good way to + * represent them in the DTO. If you need to add them, see TestRail docs for this call + * + * @param testId The ID of the test the result should be added to + * @param updateTestResultDto info about the test result to update + * @return If successful, this method returns the updated test plan entry including test runs + * using the same response format as the entries field of get_plan, but for a single entry + * instead of a list of entries. + */ + @POST("/add_result/{test_id}") + CompletableFuture> addResult( + @Path("test_id") long testId, final @Body UpdateTestResultDto updateTestResultDto); + + /** + * Adds a list new test results. + * + * @param testRailRunId The ID of the TestRail run the results should be added to + * @param testResults info about the test results to add + * @return If successful, this method returns the newly created tests for the given cases + */ + @POST("/add_results_for_cases/{run_id}") + CompletableFuture>> addResultsForCases( + @Path("run_id") long testRailRunId, final @Body AddTestResultsForCaseDto testResults); + + /** + * Returns a list of tests for a test run. The response includes an array of tests. Each test in + * this list follows the same format as get_test. NOTE: This doesn't currently support updating + * custom fields since there's no good way to represent them in the DTO. * If you need to add + * them, see TestRail docs for this call + * + * @param testRailRunId The ID of the test run + * @param statusIds optional comma-separated list of Statuses to filter by. Pass null if you don't + * want to filter + * @param offset number of items to offset the pagination by ie : an offset of 250 will skip the + * first 250 items + * @param limit max number of items to return + * @return If successful, The response includes an array of tests. Each test in this list follows + * the same format as get_test. + */ + @GET("/get_tests/{run_id}") + CompletableFuture> getTests( + @Path("run_id") long testRailRunId, + final @Query("status_id") String statusIds, + @Query("offset") int offset, + @Query("limit") int limit); + + /** + * Returns a list of test cases for a project or specific test suite (if the project has multiple + * suites enabled). + * + * @param projectId The ID of the project + * @param suiteId The id of the test suite inside the project + * @param offset number of items to offset the pagination by ie : an offset of 250 will skip the + * first 250 items + * @param limit max number of items to return + * @return If successful, The response includes an array of tests. Each test in this list follows + * the same format as get_test. + */ + @GET("/get_cases/{project_id}") + CompletableFuture> getCasesForSuite( + @Path("project_id") long projectId, + final @Query("suite_id") long suiteId, + @Query("offset") int offset, + @Query("limit") int limit); + + /** + * Returns a list of TestRail test result statuses for the calling customer. The response should + * contain the 5 default statuses, plus any other custom statuses the customer may have set up. + * + * @return list of statuses and their attributes. see TestRail API + */ + @GET("/get_statuses") + CompletableFuture>> getStatuses(); + + /** + * Returns the TestRail project matching the given id + * + * @param projectId The ID of the test project to fetch + * @return The project + */ + @GET("/get_project/{project_id}") + CompletableFuture> getProject(@Path("project_id") long projectId); + + /** + * Returns the TestRail suite with the given ID + * + * @param suiteId The ID of the test suite to fetch + * @return The test suite + */ + @GET("/get_suite/{suite_id}") + CompletableFuture> getSuite(@Path("suite_id") long suiteId); +} diff --git a/src/main/java/com/applause/auto/testrail/client/TestRailClient.java b/src/main/java/com/applause/auto/testrail/client/TestRailClient.java new file mode 100644 index 0000000..4e882f6 --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/TestRailClient.java @@ -0,0 +1,525 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client; + +import com.applause.auto.testrail.client.errors.TestRailErrorStatus; +import com.applause.auto.testrail.client.errors.TestRailException; +import com.applause.auto.testrail.client.models.internal.TestRailStatusComment; +import com.applause.auto.testrail.client.models.testrail.*; +import jakarta.ws.rs.core.Response.Status; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import javax.annotation.Nullable; +import lombok.AllArgsConstructor; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import retrofit2.Response; + +/** The TestRail client that wraps the TestRail API with exception handling */ +@AllArgsConstructor +@Slf4j +public class TestRailClient { + private static final int TESTRAIL_PAGE_LIMIT = 250; + + private final TestRailApi apiClient; + + /** + * Fetch a test plan + * + * @param planId The id of the plan to fetch from TestRail + * @return The plan object + * @throws TestRailException if there is an error response from TestRail + */ + public PlanDto getTestPlan(final long planId) throws TestRailException { + log.debug("Request getPlan from TestRail for planId [ " + planId + " ]"); + var result = this.apiClient.getPlan(planId).join(); + + if (result.code() == Status.BAD_REQUEST.getStatusCode()) { + throw new TestRailException( + "Invalid or unknown test plan: " + planId, TestRailErrorStatus.BAD_REQUEST); + } + if (result.code() == Status.FORBIDDEN.getStatusCode()) { + throw new TestRailException( + "No access to TestRail project for planId: " + planId, + TestRailErrorStatus.ACCESS_DENIED) + .setRetryable(false); + } + throwForCommonErrorStatuses(result, "Could not get TestRail test plan: " + planId); + return result.body(); + } + + /** + * Checks a TestRail response for common error statuses. These can happen even if all the + * parameters were correct. + */ + private static void throwForCommonErrorStatuses( + @NonNull final Response res, @NonNull final String detailMessage) + throws TestRailException { + if (res.isSuccessful()) { + return; + } + // try to get error body + var errorBody = + Optional.ofNullable(res.errorBody()) + .map( + thing -> { + try { + return thing.string(); + } catch (IOException e) { + log.warn("error thrown during attempt to read TestRail error response body", e); + return ""; + } + }) + .orElse("[unavailable]"); + + var errorDetailMsg = detailMessage + " Error from testrail: " + errorBody; + + switch (Status.fromStatusCode(res.code())) { + case CONFLICT -> throw new TestRailException(errorDetailMsg, TestRailErrorStatus.MAINTENANCE); + case UNAUTHORIZED -> throw new TestRailException( + errorDetailMsg, TestRailErrorStatus.AUTHENTICATION_FAILED); + case TOO_MANY_REQUESTS -> throw new TestRailException( + errorDetailMsg, TestRailErrorStatus.HIT_RATE_LIMIT); + case NOT_FOUND -> { + try (var rawRes = res.raw()) { + throw new TestRailException( + "Invalid Route: %s Error from testrail: %s" + .formatted(rawRes.request().url().url().getPath(), errorBody), + TestRailErrorStatus.INVALID_ROUTE); + } + } + default -> throw new TestRailException(errorDetailMsg, TestRailErrorStatus.UNKNOWN_ERROR); + } + } + + /** + * Retrieve the statuses for the given TestRail account + * + * @return A list of the statuses for the given TestRail account + * @throws TestRailException if there is an error response from TestRail + */ + public List getCustomStatuses() throws TestRailException { + log.debug("Requesting getStatuses from TestRail"); + var result = this.apiClient.getStatuses().join(); + debugLogPostResponse(result.code(), "getStatuses"); + + throwForCommonErrorStatuses(result, "Could not get TestRail statuses"); + return result.body(); + } + + /** + * Fetch a given project + * + * @param projectId The id of the project to fetch from TestRail + * @return The project object + * @throws TestRailException if there is an error response from TestRail + */ + public ProjectDto getProject(final long projectId) throws TestRailException { + log.debug("Requesting getProject from TestRail for projectId [ " + projectId + " ]"); + var result = this.apiClient.getProject(projectId).join(); + debugLogPostResponse(result.code(), "getProject"); + + if (result.code() == Status.BAD_REQUEST.getStatusCode()) { + throw new TestRailException( + "Invalid or unknown project: " + projectId, TestRailErrorStatus.BAD_REQUEST); + } + if (result.code() == Status.FORBIDDEN.getStatusCode()) { + throw new TestRailException( + "No access to TestRail project: " + projectId, TestRailErrorStatus.ACCESS_DENIED) + .setRetryable(false); + } + throwForCommonErrorStatuses(result, "Could not get TestRail project: " + projectId); + + return result.body(); + } + + /** + * Get a given test suite + * + * @param suiteId The suiteId to fetch + * @return The test suite + * @throws TestRailException if there is an error response from TestRail + */ + public TestSuiteDto getTestSuite(final long suiteId) throws TestRailException { + log.debug("Requesting getSuite from TestRail for suiteId [ " + suiteId + " ]"); + var result = this.apiClient.getSuite(suiteId).join(); + debugLogPostResponse(result.code(), "getSuite"); + + if (result.code() == Status.BAD_REQUEST.getStatusCode()) { + throw new TestRailException( + "Invalid or unknown test suite: " + suiteId, TestRailErrorStatus.BAD_REQUEST); + } + if (result.code() == Status.FORBIDDEN.getStatusCode()) { + throw new TestRailException( + "No access to TestRail project for suiteId: " + suiteId, + TestRailErrorStatus.ACCESS_DENIED) + .setRetryable(false); + } + throwForCommonErrorStatuses(result, "Could not get TestRail test suite: " + suiteId); + + return result.body(); + } + + /** + * Creates a test plan in TestRail + * + * @param testPlanName TestRail plan name + * @param testRailProjectId TestRail project id to create plan for + * @return the created TestRail plan + * @throws TestRailException if there is an error response from TestRail + */ + public PlanDto createTestPlan(@NonNull final String testPlanName, final long testRailProjectId) + throws TestRailException { + var dto = AddPlanDto.builder().name(testPlanName).build(); + + log.debug( + "Request addPlan from TestRail for projectId [ " + + testRailProjectId + + " ] planName [ " + + testPlanName + + " ]"); + var result = this.apiClient.addPlan(testRailProjectId, dto).join(); + debugLogPostResponse(result.code(), "addPlan"); + + if (result.code() == Status.BAD_REQUEST.getStatusCode()) { + throw new TestRailException( + "Invalid or unknown project: " + testRailProjectId, TestRailErrorStatus.BAD_REQUEST); + } + if (result.code() == Status.FORBIDDEN.getStatusCode()) { + throw new TestRailException( + "No permission to add test plans or no access to TestRail project.", + TestRailErrorStatus.ACCESS_DENIED) + .setRetryable(false); + } + throwForCommonErrorStatuses( + result, "Could not add TestRail plan for project id: " + testRailProjectId); + return result.body(); + } + + /** + * Adds a result for a given testId + * + * @param testRailRunId The id of the test run to add the results for + * @param results A map of caseId to status and comment + * @return the result for the test + * @throws TestRailException if there is an error response from TestRail + */ + public List addResults( + final long testRailRunId, @NonNull final Map results) + throws TestRailException { + final AddTestResultsForCaseDto testResults = new AddTestResultsForCaseDto(); + for (Map.Entry result : results.entrySet()) { + testResults.add( + AddTestResultForCaseDto.builder() + .caseId(result.getKey()) + .statusId(result.getValue().statusId()) + .comment(result.getValue().comment()) + .build()); + } + + log.debug( + "Sending addResultsForCases request to TestRail for run [{}] with test results count [{}]", + testRailRunId, + results.size()); + var res = this.apiClient.addResultsForCases(testRailRunId, testResults).join(); + debugLogPostResponse(res.code(), "addResultsForCases"); + + if (res.code() == Status.BAD_REQUEST.getStatusCode()) { + throw new TestRailException( + "Invalid or unknown run id: " + testRailRunId, TestRailErrorStatus.BAD_REQUEST); + } + if (res.code() == Status.FORBIDDEN.getStatusCode()) { + throw new TestRailException( + "No permission to add test result or no access to TestRail project.", + TestRailErrorStatus.ACCESS_DENIED); + } + throwForCommonErrorStatuses(res, "Could not add TestRail result for run id: " + testRailRunId); + return res.body(); + } + + /** + * Adds a result for a given testId + * + * @param projectId The id of the project in TestRail + * @param suiteId The id of the suite in TestRail + * @return the result for the test + * @throws TestRailException if there is an error response from TestRail + */ + public List getTestCasesForSuite(final long projectId, final long suiteId) + throws TestRailException { + log.debug( + "Requesting getCasesForSuite from TestRail for project [ " + + projectId + + " ] suiteId [ " + + suiteId + + " ]"); + int offset = 0; + boolean nextPage = true; + List cases = new ArrayList<>(); + while (nextPage) { + var res = + this.apiClient.getCasesForSuite(projectId, suiteId, offset, TESTRAIL_PAGE_LIMIT).join(); + nextPage = false; + offset += TESTRAIL_PAGE_LIMIT; + debugLogPostResponse(res.code(), "getCasesForSuite"); + + if (res.code() == Status.BAD_REQUEST.getStatusCode()) { + throw new TestRailException( + "Invalid or unknown project [%d] or suite [%d]".formatted(projectId, suiteId), + TestRailErrorStatus.BAD_REQUEST); + } + if (res.code() == Status.FORBIDDEN.getStatusCode()) { + throw new TestRailException( + "No permission to get this suite or no access to TestRail project", + TestRailErrorStatus.ACCESS_DENIED); + } + + throwForCommonErrorStatuses(res, "Could not fetch test case for suite: " + suiteId); + var body = res.body(); + if (body != null && body.cases() != null && !body.cases().isEmpty()) { + cases.addAll(body.cases()); + } + if (body != null + && body._links() != null + && body._links().next() != null + && body.size() > 0) { + nextPage = true; + } + } + return cases; + } + + /** + * Create a new plan entry in the current plan, and add a test case. This equates to a test run + * + * @param testRailPlanId TestRail plan name + * @param testRunName TestRail run name + * @param testSuiteId TestRail suite id + * @param includeAll Whether to add all tests to the new plan entry + * @param testCaseIds A set of ids to create the plan with + * @return the new plan entry + * @throws TestRailException if there is an error response from TestRail + */ + public PlanEntryDto createNewPlanEntry( + @NonNull final String testRunName, + final long testSuiteId, + final long testRailPlanId, + final boolean includeAll, + @NonNull final Set testCaseIds) + throws TestRailException { + + if (testCaseIds.isEmpty()) { + throw new TestRailException( + "TestcaseId invalid. PlanEntry creation must contain at least 1 valid Testcase Id.", + TestRailErrorStatus.BAD_REQUEST) + .setRetryable(false); + } + + final var dto = + AddPlanEntryDto.builder() + .suiteId(testSuiteId) + .includeAll(includeAll) + .caseIds(testCaseIds) + .name(testRunName) + .build(); + + log.debug( + "Requesting addPlanEntry from TestRail for plan [{}], suite [{}], run [{}], and test case id count [{}]", + testRailPlanId, + testSuiteId, + testRunName, + testCaseIds.size()); + + var result = this.apiClient.addPlanEntry(testRailPlanId, dto).join(); + debugLogPostResponse(result.code(), "addPlanEntry"); + + if (result.code() == Status.BAD_REQUEST.getStatusCode()) { + throw new TestRailException( + "Invalid or unknown test plan id: " + testRailPlanId, TestRailErrorStatus.BAD_REQUEST); + } + if (result.code() == Status.FORBIDDEN.getStatusCode()) { + throw new TestRailException( + "No permission to modify test plans or no access to TestRail project.", + TestRailErrorStatus.ACCESS_DENIED) + .setRetryable(false); + } + throwForCommonErrorStatuses( + result, "Could not add TestRail plan entry for test plan id: " + testRailPlanId); + return result.body(); + } + + /** + * Updates an existing TestRail plan entry + * + * @param planId The plan id for the plan entry you want to update + * @param planEntryId The id of the plan entry to update + * @param caseIds A list of caseIds for all the cases + * @return The Updated TestRail Plan Entry + * @throws TestRailException if there is an error response from TestRail + */ + public PlanEntryDto updateExistingPlanEntry( + final long planId, @NonNull final String planEntryId, @Nullable final Set caseIds) + throws TestRailException { + log.debug( + "Requesting updatePlanEntry from TestRail for planId [ " + + planId + + " ] planEntryId [ " + + planEntryId + + " ] caseId count [ " + + (caseIds != null ? caseIds.size() : 0) + + " ]"); + var result = + this.apiClient + .updatePlanEntry( + planId, planEntryId, UpdatePlanEntryDto.builder().caseIds(caseIds).build()) + .join(); + debugLogPostResponse(result.code(), "updatePlanEntry"); + + if (result.code() == Status.BAD_REQUEST.getStatusCode()) { + throw new TestRailException( + "Bad request for test plan: " + planId + " of test plan entry: " + planEntryId, + TestRailErrorStatus.BAD_REQUEST); + } + if (result.code() == Status.FORBIDDEN.getStatusCode()) { + throw new TestRailException( + "No permission to add test result or no access to TestRail project.", + TestRailErrorStatus.ACCESS_DENIED) + .setRetryable(false); + } + throwForCommonErrorStatuses( + result, "Could not update TestRail plan entry " + planEntryId + "in plan " + planId); + + return result.body(); + } + + /** + * Fetch a test plan + * + * @param projectId The id of the project to fetch from TestRail + * @param planName The name of the plan to search for + * @return The plan object + * @throws TestRailException if there is an error response from TestRail + */ + public Optional findExistingTestPlan( + final long projectId, @NonNull final String planName) throws TestRailException { + log.debug( + "Requesting getPlansForProject from TestRail for projectId [ " + + projectId + + " ] planName [ " + + planName + + " ]"); + + int offset = 0; + boolean nextPage = true; + List plans = new ArrayList<>(); + while (nextPage) { + var result = this.apiClient.getPlansForProject(projectId, offset, TESTRAIL_PAGE_LIMIT).join(); + nextPage = false; + offset += TESTRAIL_PAGE_LIMIT; + debugLogPostResponse(result.code(), "getPlansForProject"); + + if (result.code() == Status.BAD_REQUEST.getStatusCode()) { + throw new TestRailException( + "Invalid or unknown project: " + projectId, TestRailErrorStatus.BAD_REQUEST); + } + if (result.code() == Status.FORBIDDEN.getStatusCode()) { + throw new TestRailException( + "No access to TestRail project: " + projectId, TestRailErrorStatus.ACCESS_DENIED) + .setRetryable(false); + } + throwForCommonErrorStatuses(result, "Could not get TestRail test project: " + projectId); + + var body = result.body(); + if (body != null && body.plans() != null && !body.plans().isEmpty()) { + plans.addAll(body.plans()); + } + if (body != null + && body._links() != null + && body._links().next() != null + && body.size() > 0) { + nextPage = true; + } + } + + return plans.parallelStream() + .filter(plan -> !plan.getIsCompleted()) + .filter(plan -> plan.getName().equals(planName)) + .min((planA, planB) -> Math.toIntExact(planB.getCreatedOn() - planA.getCreatedOn())); + } + + /** + * Get the test results for a run + * + * @param testRailRunId The run id to fetch the results for + * @param statusIds A CSV string of status ids to search the results for + * @return A list of tests for the given TestRail run with the matching status ids + * @throws TestRailException if there is an error response from TestRail + */ + public List getTestResultsForRun( + final long testRailRunId, @Nullable final String statusIds) throws TestRailException { + log.debug( + "Requesting getTests from TestRail for testRailRunId [ " + + testRailRunId + + " ] containing statusIds [ " + + statusIds + + " ]"); + + int offset = 0; + boolean nextPage = true; + List tests = new ArrayList<>(); + while (nextPage) { + var result = + this.apiClient.getTests(testRailRunId, statusIds, offset, TESTRAIL_PAGE_LIMIT).join(); + nextPage = false; + offset += TESTRAIL_PAGE_LIMIT; + debugLogPostResponse(result.code(), "getTests"); + + if (result.code() == Status.BAD_REQUEST.getStatusCode()) { + throw new TestRailException( + "Invalid or unknown test run id: " + testRailRunId, TestRailErrorStatus.BAD_REQUEST); + } + if (result.code() == Status.FORBIDDEN.getStatusCode()) { + throw new TestRailException( + "No access to TestRail project for testRailRunId: " + testRailRunId, + TestRailErrorStatus.ACCESS_DENIED); + } + throwForCommonErrorStatuses( + result, "Could not get TestRail test list for test run id: " + testRailRunId); + + var body = result.body(); + if (body != null && body.tests() != null && !body.tests().isEmpty()) { + tests.addAll(body.tests()); + } + if (body != null + && body._links() != null + && body._links().next() != null + && body.size() > 0) { + nextPage = true; + } + } + return tests; + } + + private void debugLogPostResponse(final int httpStatusCode, @NonNull final String methodName) { + log.debug("Retrieved HTTP " + httpStatusCode + " from TestRail " + methodName + " request."); + } +} diff --git a/src/main/java/com/applause/auto/testrail/client/TestRailClientFactory.java b/src/main/java/com/applause/auto/testrail/client/TestRailClientFactory.java new file mode 100644 index 0000000..b037e93 --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/TestRailClientFactory.java @@ -0,0 +1,76 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client; + +import com.applause.auto.testrail.client.interceptors.GenericErrorInterceptor; +import com.applause.auto.testrail.client.interceptors.HeadersInterceptor; +import com.applause.auto.testrail.client.interceptors.UrlInterceptor; +import com.applause.auto.testrail.client.models.config.TestRailConfig; +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import lombok.AllArgsConstructor; +import okhttp3.OkHttpClient; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; +import retrofit2.converter.scalars.ScalarsConverterFactory; + +/** A TestRail Client Factory for initializing TestRail API Clients from a base OkHttp Client */ +@AllArgsConstructor +public class TestRailClientFactory { + private static final Gson gson = + new GsonBuilder() + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .create(); + + private final OkHttpClient baseHttpClient; + + /** + * Gets the base TestRail API for the provided config + * + * @param config The TestRail Config + * @return The TestRail API Instance + */ + public TestRailApi getTestRailApi(final TestRailConfig config) { + final var httpClient = + baseHttpClient + .newBuilder() + .addInterceptor(new HeadersInterceptor(config.getEmail(), config.getApiKey())) + .addInterceptor(new UrlInterceptor()) + .addInterceptor(new GenericErrorInterceptor()) + .build(); + return new Retrofit.Builder() + .baseUrl(config.getUrl()) + .client(httpClient) + .addConverterFactory(ScalarsConverterFactory.create()) + .addConverterFactory(GsonConverterFactory.create(gson)) + .build() + .create(TestRailApi.class); + } + + /** + * Gets the base TestRail Client for the provided config + * + * @param config The TestRail Config + * @return The TestRail Client Instance + */ + public TestRailClient getTestRailClient(final TestRailConfig config) { + final var apiClient = this.getTestRailApi(config); + return new TestRailClient(apiClient); + } +} diff --git a/src/main/java/com/applause/auto/testrail/client/TestRailParamValidator.java b/src/main/java/com/applause/auto/testrail/client/TestRailParamValidator.java new file mode 100644 index 0000000..02863c6 --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/TestRailParamValidator.java @@ -0,0 +1,210 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client; + +import com.applause.auto.testrail.client.errors.TestRailErrorStatus; +import com.applause.auto.testrail.client.errors.TestRailException; +import com.applause.auto.testrail.client.models.config.TestRailStatusMaps; +import com.applause.auto.testrail.client.models.testrail.ProjectDto; +import com.applause.auto.testrail.client.models.testrail.StatusDto; +import com.applause.auto.testrail.client.models.testrail.TestCaseDto; +import com.applause.auto.testrail.client.models.testrail.TestSuiteDto; +import com.google.common.collect.ImmutableSet; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import lombok.AllArgsConstructor; +import lombok.NonNull; +import org.apache.commons.lang3.StringUtils; + +/** Helper for validating the TestRail parameters */ +@AllArgsConstructor +public class TestRailParamValidator { + private final TestRailClient testRailClient; + + /** + * Validates the given TestRail Project + * + * @param testRailProjectId The id of the project to validate + * @return The TestRail Project + * @throws TestRailException if we can't connect to TestRail or validation fails + */ + public ProjectDto validateTestRailProjectId(long testRailProjectId) throws TestRailException { + // Check that the project exists in TestRail and that we have access to it. + ProjectDto project = this.testRailClient.getProject(testRailProjectId); + + // Verify that the project isn't marked as completed. + if (project.isCompleted()) { + throw new TestRailException( + "Test Rail Project is Completed.", TestRailErrorStatus.ACCESS_DENIED); + } + + return project; + } + + /** + * Validate a test suite + * + * @param suiteId The id of the test suite + * @param testRailProjectId The project id that the suite maps to + * @return The TestSuite + * @throws TestRailException if the validation fails or the suite could not be grabbed. + */ + TestSuiteDto validateTestRailSuite(final long suiteId, final long testRailProjectId) + throws TestRailException { + // Try to grab the test suite from TestRail, verifying that it exists, and we have access to + // view + // it. + TestSuiteDto suite = this.testRailClient.getTestSuite(suiteId); + + // Verify that the suite has not been marked as completed. + if (suite.isCompleted()) { + throw new TestRailException( + "Test Rail Suite is marked as completed", TestRailErrorStatus.ACCESS_DENIED); + } + + // Verify that the suite is for the projectId that was passed in. + if (suite.getProjectId() != testRailProjectId) { + throw new TestRailException( + "Test Rail Suite does not match the given project", TestRailErrorStatus.ACCESS_DENIED); + } + + return suite; + } + + /** + * Perform some internal validation of TestRail status codes, which have some peculiar logic. + * + *

The biggest 'gotcha' being that TestRail gives us an 'Untested' status but doesn't actually + * allow it to be used. 2nd 'gotcha' is that TestRail configuration is changed outside of + * Applause's-TestRail configuration - so they may get out of sync. (eg: we have old status codes) + * + * @return List of strings reporting failed validation issues. Empty list of none. + * @throws TestRailException Failed validation issues. + */ + private List verifyResultStatusCodes(final TestRailStatusMaps statusMaps) + throws TestRailException { + List results = new ArrayList<>(); + var statusList = this.testRailClient.getCustomStatuses(); + + Set statusIds = statusList.stream().map(StatusDto::getId).collect(Collectors.toSet()); + + if (!statusIds.containsAll(statusMaps.getTestRailStatusIds())) { + results.add( + "Applause TestRail Status codes do not match TestRail status codes. Expect problems adding results to TestRail."); + } + + // UNTESTED is internal to TestRail, and they disallow setting status as a result type + if (statusMaps.getTestRailStatusIds().contains(TestRailUtil.TESTRAIL_UNTESTED_STATUS_ID)) { + results.add( + "Applause TestRail configured for invalid status code 3 (Untested). Expect problems adding results to TestRail."); + } + return results; + } + + /** + * Validates the given TestRail Parameters + * + * @param testRailProjectId The id of the TestRailProject + * @param testRailSuiteId The id of the TestRail suite + * @param testRailPlanName The name of the plan to use/create + * @param testRailRunName The name of the run to create + * @throws TestRailException if pre-validation fails + */ + public void validateTestRailParams( + @Nullable final Long testRailProjectId, + @Nullable final Long testRailSuiteId, + @Nullable final String testRailPlanName, + @Nullable final String testRailRunName) + throws TestRailException { + + // Otherwise, check all parameters + final List preValidationErrors = new ArrayList<>(); + + if (StringUtils.isBlank(testRailPlanName)) { + preValidationErrors.add("Invalid testRailPlanName: " + testRailPlanName); + } + if (StringUtils.isBlank(testRailRunName)) { + preValidationErrors.add("Invalid testRailRunName: " + testRailRunName); + } + if (testRailProjectId == null || testRailProjectId < 0) { + preValidationErrors.add("Invalid testRailProjectId: " + testRailProjectId); + } + if (testRailSuiteId == null || testRailSuiteId < 0) { + preValidationErrors.add("Invalid testRailSuiteId: " + testRailSuiteId); + } + if (!preValidationErrors.isEmpty()) { + throw new TestRailException( + String.join(", ", preValidationErrors), TestRailErrorStatus.BAD_REQUEST); + } + this.validateTestRailProjectId(testRailProjectId); + this.validateTestRailSuite(testRailSuiteId, testRailProjectId); + } + + /** + * Validates the provided TestRail configuration + * + * @param testRailProjectId The project ID to validate + * @param testRailSuiteId The suite id to validate + * @param testRailPlanName The plan name to validate + * @param testRailRunName The run name to validate + * @param statusMaps The TestRail status mapping + * @throws TestRailException If validation fails + */ + public void validateTestrailConfiguration( + @Nullable final Long testRailProjectId, + @Nullable final Long testRailSuiteId, + @Nullable final String testRailPlanName, + @Nullable final String testRailRunName, + @Nullable final TestRailStatusMaps statusMaps) + throws TestRailException { + validateTestRailParams(testRailProjectId, testRailSuiteId, testRailPlanName, testRailRunName); + if (statusMaps != null) { + verifyResultStatusCodes(statusMaps); + } + } + + void validateTestCaseIds( + final long projectId, final long suiteId, @NonNull final Set testrailCaseIds) + throws TestRailException { + final ImmutableSet testCasesInTestRail = + this.testRailClient.getTestCasesForSuite(projectId, suiteId).stream() + .map(TestCaseDto::getId) + .collect(ImmutableSet.toImmutableSet()); + for (final String caseId : testrailCaseIds) { + if (caseId == null) { + continue; + } + // make sure its valid first + if (!TestRailUtil.validateTestRailCaseId(caseId)) { + throw new TestRailException( + "Test case ID " + caseId + " is invalid", TestRailErrorStatus.CASE_ID); + } + // trim it + long filteredTestCaseId = TestRailUtil.extractTestCaseId(caseId); + // make sure case exists on testrail + if (!testCasesInTestRail.contains(filteredTestCaseId)) { + throw new TestRailException( + "Test Case ID " + filteredTestCaseId + " has no matching case in Testrail", + TestRailErrorStatus.CASE_ID); + } + } + } +} diff --git a/src/main/java/com/applause/auto/testrail/client/TestRailResultLogger.java b/src/main/java/com/applause/auto/testrail/client/TestRailResultLogger.java new file mode 100644 index 0000000..2a8720d --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/TestRailResultLogger.java @@ -0,0 +1,273 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client; + +import com.applause.auto.testrail.client.errors.TestRailException; +import com.applause.auto.testrail.client.models.internal.TestRailRunsAndInvalidCases; +import com.applause.auto.testrail.client.models.internal.TestRailStatusComment; +import com.applause.auto.testrail.client.models.internal.TestRailValidateRequest; +import com.applause.auto.testrail.client.models.testrail.PlanDto; +import com.applause.auto.testrail.client.models.testrail.TestCaseDto; +import com.applause.auto.testrail.client.models.testrail.TestDto; +import com.applause.auto.testrail.client.models.testrail.TestRunDto; +import com.google.common.collect.Table; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import lombok.AllArgsConstructor; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +/** Handles reporting results to TestRail */ +@AllArgsConstructor +@Slf4j +public class TestRailResultLogger { + private final TestRailClient testRailClient; + + /** + * Executes the requests against TestRail. This will set up or verify that all the objects on + * TestRail side, so we can post the results. All the initialization should be performed in + * resultsToLogByRunNameAndCaseId. + * + * @param resultsToLogByRunNameAndCaseId Multi-key map of Results by runName and caseId + * @param validateRequest the object with params required for posting to testrail. project, suite, + * etc + * @return the invalid testcaseIds + * @throws TestRailException if execution fails + */ + public Set execute( + @NonNull final Table resultsToLogByRunNameAndCaseId, + @NonNull final TestRailValidateRequest validateRequest) + throws TestRailException { + log.debug( + "Starting TestRail reporting for suite {} plan {} planName {}", + validateRequest.suiteId(), + validateRequest.planId(), + validateRequest.planName()); + List testCases = + this.testRailClient.getTestCasesForSuite( + validateRequest.projectId(), validateRequest.suiteId()); + + log.trace("Verifying TestRail setup."); + // Verify that all the plans / runs are set up correctly on TestRail's end + final var runsAndInvalidCases = + this.verifyAndInitializeSetup(testCases, resultsToLogByRunNameAndCaseId, validateRequest); + + for (final var runEntry : runsAndInvalidCases.getRunDtosByName().entrySet()) { + String runEntryKey = runEntry.getKey(); + Long runEntryValueId = runEntry.getValue().getId(); + this.testRailClient.addResults( + runEntryValueId, resultsToLogByRunNameAndCaseId.row(runEntryKey)); + } + + // Let the caller know if there were some case ids we filtered out + // these will end up error out, so we don't try to re-log them again + return runsAndInvalidCases.getInvalidCaseIds(); + } + + private TestRailRunsAndInvalidCases verifyAndInitializeSetup( + @NonNull final List testCases, + @NonNull final Table resultsToLogByRunNameAndCaseId, + @NonNull final TestRailValidateRequest validateRequest) + throws TestRailException { + final TestRailRunsAndInvalidCases runsAndInvalidCases = new TestRailRunsAndInvalidCases(); + + log.trace( + "Filtering out bad caseIds for project " + + validateRequest.projectId() + + " and suite " + + validateRequest.suiteId()); + // Collect the set of all case ids in the suite, and all the ones we are trying to add + final var caseIdsForSuite = + testCases.stream().map(TestCaseDto::getId).collect(Collectors.toSet()); + final var invalidCaseIds = + this.filterOutBadCaseIds(caseIdsForSuite, resultsToLogByRunNameAndCaseId); + runsAndInvalidCases.setInvalidCaseIds(invalidCaseIds); + + // Verify Plan + final PlanDto planDto = + this.verifyOrCreatePlan( + validateRequest.projectId(), validateRequest.planId(), validateRequest.planName()); + + final Map runDtosByName = new HashMap<>(); + + // Each driver gets mapped to a different run. + for (final String runName : resultsToLogByRunNameAndCaseId.rowKeySet()) { + // Verify Runs + final TestRunDto runDto = + this.verifyOrCreateRun(validateRequest, resultsToLogByRunNameAndCaseId, planDto, runName); + runDtosByName.put(runName, runDto); + + // Verify Case Ids + this.verifyCaseIdsAreSetupForRun( + resultsToLogByRunNameAndCaseId, planDto.getId(), runDto, runName); + } + runsAndInvalidCases.setRunDtosByName(runDtosByName); + return runsAndInvalidCases; + } + + private Set filterOutBadCaseIds( + @NonNull final Set caseIdsForSuite, + @NonNull final Table resultsToLogByRunNameAndCaseId) { + final var requestedCaseIds = resultsToLogByRunNameAndCaseId.columnKeySet(); + + // Get a list of all requested case ids that do not belong to the given suite + final var invalidCaseIds = new HashSet<>(requestedCaseIds); + invalidCaseIds.removeAll(caseIdsForSuite); + + // If they are empty, we don't need to do anything else. + if (invalidCaseIds.isEmpty()) { + log.trace("No invalid caseIds"); + return Collections.emptySet(); + } + + // In this case we have at least one bad caseId, we need to remove them from the results + // map, so they don't get logged to TestRail. + for (final var resultSet : resultsToLogByRunNameAndCaseId.rowMap().values()) { + log.trace("Removing invalid caseIds " + StringUtils.join(invalidCaseIds, ",")); + resultSet.keySet().removeAll(invalidCaseIds); + } + + return invalidCaseIds; + } + + private TestRunDto verifyOrCreateRun( + @NonNull final TestRailValidateRequest validateRequest, + @NonNull final Table resultsToLogByRunNameAndCaseId, + @NonNull final PlanDto planDtoMaybeIncomplete, + @NonNull final String runName) + throws TestRailException { + log.trace( + "Verifying Runs are setup for runName " + + runName + + " for plan " + + planDtoMaybeIncomplete.getId()); + + try { + // If the entries aren't populated, we need to do a second lookup to populate them. + // This may happen if we just created the run. + final PlanDto completePlan = + Objects.nonNull(planDtoMaybeIncomplete.getEntries()) + ? planDtoMaybeIncomplete + : this.testRailClient.getTestPlan(planDtoMaybeIncomplete.getId()); + + final var resultsToLog = resultsToLogByRunNameAndCaseId.row(runName); + + // Try to see if a run already exists with the same name. Otherwise, create one + final var existingRun = + completePlan.getEntries().stream() + .flatMap(e -> e.getRuns().stream()) + .filter(r -> !r.getIsCompleted()) + .filter(r -> runName.equals(r.getName())) + .findFirst(); + if (existingRun.isPresent()) { + return existingRun.get(); + } + return this.testRailClient + .createNewPlanEntry( + runName, + validateRequest.suiteId(), + completePlan.getId(), + validateRequest.includeAll(), + resultsToLog.keySet()) + .getRuns() + .get(0); + } catch (TestRailException e) { + log.error( + "Caught Error for Project/Plan [{}/{}]", + validateRequest.projectId(), + validateRequest.planId(), + e); + throw e; + } + } + + /** + * Get or Create TestRail TestPlan from TestRail provider + * + * @param projectId projectId containing plan + * @param planId The planId if available + * @param planName The planName to search or create + * @return The TestRail PlanDto + * @throws TestRailException If plan creation fails + */ + public PlanDto verifyOrCreatePlan( + @NonNull final Long projectId, @Nullable final Long planId, @NonNull final String planName) + throws TestRailException { + log.trace("VerifyingOrCreate Plan is setup for plan " + planId); + // If the plan id is non-null, then the plan has already been set up by a previous iteration. + // Just fetch the plan, so we can look at the runs later. + if (Objects.nonNull(planId)) { + return this.testRailClient.getTestPlan(planId); + } + + final var cleanTestPlanName = TestRailUtil.cleanTestRailPlanName(planName); + + // First, attempt to find a pre-created plan by name. Otherwise, create a new one + var existingPlan = this.testRailClient.findExistingTestPlan(projectId, cleanTestPlanName); + if (existingPlan.isPresent()) { + return existingPlan.get(); + } + return this.testRailClient.createTestPlan(cleanTestPlanName, projectId); + } + + private void verifyCaseIdsAreSetupForRun( + @NonNull final Table resultsToLogByRunNameAndCaseId, + @NonNull final Long planId, + @NonNull final TestRunDto testRunDto, + @NonNull final String runName) + throws TestRailException { + log.trace("Verifying caseIds are setup for run " + runName); + + // Fetch all the TestResults for the given run + final var testResults = this.testRailClient.getTestResultsForRun(testRunDto.getId(), null); + final var resultsToLog = resultsToLogByRunNameAndCaseId.row(runName); + + // Get a list of all existing case ids in the results + Set existingCaseIds = + testResults.stream().map(TestDto::getCaseId).collect(Collectors.toSet()); + + // Compare this to the results we are trying to log. In some cases, we have already added + // a case id to the plan (Ex. retries) + Set newCaseIds = new HashSet<>(resultsToLog.keySet()); + newCaseIds.removeAll(existingCaseIds); + + // if test case isn't part of run (and so isn't part of plan entry either), add it to plan entry + if (!newCaseIds.isEmpty()) { + log.debug( + "couldn't find matching test case IDs " + + StringUtils.join(newCaseIds, ", ") + + " in run " + + testRunDto.getId() + + ". Adding test cases to run"); + + // Add back the existing caseIds, so we don't remove results + newCaseIds.addAll(existingCaseIds); + + // call update endpoint with existing case ID's and new ones + this.testRailClient.updateExistingPlanEntry(planId, testRunDto.getEntryId(), newCaseIds); + } + } +} diff --git a/src/main/java/com/applause/auto/testrail/client/TestRailResultUploader.java b/src/main/java/com/applause/auto/testrail/client/TestRailResultUploader.java new file mode 100644 index 0000000..311a25e --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/TestRailResultUploader.java @@ -0,0 +1,254 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client; + +import com.applause.auto.testrail.client.enums.TestResultStatus; +import com.applause.auto.testrail.client.errors.TestRailException; +import com.applause.auto.testrail.client.models.config.TestRailConfig; +import com.applause.auto.testrail.client.models.config.TestRailConfigExtended; +import com.applause.auto.testrail.client.models.config.TestRailStatusMaps; +import com.applause.auto.testrail.client.models.internal.TestRailStatusComment; +import com.applause.auto.testrail.client.models.internal.TestRailValidateRequest; +import com.applause.auto.testrail.client.models.testrail.StatusDto; +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Table; +import java.net.Proxy; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import javax.annotation.Nullable; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.NonNull; +import okhttp3.OkHttpClient; + +/** Handles uploading result to TestRail */ +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class TestRailResultUploader { + @NonNull private final TestRailConfigExtended testRailConfigExtended; + @NonNull private final ProjectConfiguration projectConfiguration; + @NonNull private final TestRailResultLogger testRailResultLogger; + private final long testRailPlanId; + @NonNull private final TestRailParamValidator paramValidator; + + /** + * @param testRailConfig mostly credentials + * @param projectConfiguration project specific configuration + * @param proxyConfig MAKE SURE TO PASS SDK PROXY CONFIG. YOU CAN GET THIS WITH + * ApplauseConfigHelper.getHttpProxy(ApplauseEnvironmentConfigurationManager.get()) . I didn't + * just call it in here because this submodule can't depend on SDK code (it's used in auto-api + * too) + * @return The TestRailResultUploader + * @throws TestRailException if initialization fails + */ + public static TestRailResultUploader initialize( + @NonNull final TestRailConfig testRailConfig, + @NonNull final ProjectConfiguration projectConfiguration, + @Nullable final Proxy proxyConfig) + throws TestRailException { + var httpClient = + new OkHttpClient.Builder() + .proxy(proxyConfig != null ? proxyConfig : Proxy.NO_PROXY) + .build(); + + // testrail statuses are ints in their API, but strings in the SDK config file. We need to map + // them over + final var client = new TestRailClientFactory(httpClient).getTestRailClient(testRailConfig); + var statusesFromTestRail = client.getCustomStatuses(); + var testRailConfigExtended = + testRailConfig.toExtended( + getTestRailStatusMaps(projectConfiguration, statusesFromTestRail)); + var paramValidator = new TestRailParamValidator(client); + + // then call validateTestrailConfiguration to validate everything + paramValidator.validateTestrailConfiguration( + projectConfiguration.testRailProjectId(), + projectConfiguration.testRailSuiteId(), + projectConfiguration.testRailPlanName(), + projectConfiguration.testRailRunName(), + null); + + var resultLogger = new TestRailResultLogger(client); + // grab planId on init, so we don't do it repeatedly during result upload + // yeah this is pretty awkward API but whatever. We call this with null planId to force lookup, + // then grab ID for later so we don't need to lookup plan ID for every result we upload + var testRailPlanId = + resultLogger + .verifyOrCreatePlan( + projectConfiguration.testRailProjectId(), + null, + projectConfiguration.testRailPlanName()) + .getId(); + // make instance of this class + return new TestRailResultUploader( + testRailConfigExtended, projectConfiguration, resultLogger, testRailPlanId, paramValidator); + } + + private static TestRailStatusMaps getTestRailStatusMaps( + ProjectConfiguration projectConfiguration, List statusesFromTestRail) { + var testRailStatusMap = + statusesFromTestRail.stream() + .collect( + ImmutableMap.toImmutableMap( + status -> status.getName().toLowerCase().trim(), + Function.identity(), + (first, second) -> { + throw new RuntimeException( + "TestRail has multiple result statuses configured with system name [" + + first.getName() + + "] and [" + + second.getName() + + "] Duplicates detected have status ID's " + + first.getId() + + " and " + + second.getId()); + })); + // try to map the status strings in config bean to ints in testrail + return new TestRailStatusMaps( + Optional.ofNullable( + testRailStatusMap.get(projectConfiguration.statusPassed().toLowerCase().trim())) + .map(StatusDto::getId) + .orElseThrow( + () -> + new RuntimeException( + "Mapping for TestRail Passed Status " + + projectConfiguration.statusPassed().toLowerCase().trim() + + " not found")), + Optional.ofNullable( + testRailStatusMap.get(projectConfiguration.statusFailed().toLowerCase().trim())) + .map(StatusDto::getId) + .orElseThrow( + () -> + new RuntimeException( + "Mapping for TestRail Failed Status " + + projectConfiguration.statusFailed().toLowerCase().trim() + + " not found")), + Optional.ofNullable( + testRailStatusMap.get(projectConfiguration.statusSkipped().toLowerCase().trim())) + .map(StatusDto::getId) + .orElseThrow( + () -> + new RuntimeException( + "Mapping for TestRail Skipped Status " + + projectConfiguration.statusSkipped().toLowerCase().trim() + + " not found")), + Optional.ofNullable( + testRailStatusMap.get(projectConfiguration.statusError().toLowerCase().trim())) + .map(StatusDto::getId) + .orElseThrow( + () -> + new RuntimeException( + "Mapping for TestRail Error Status " + + projectConfiguration.statusError().toLowerCase().trim() + + " not found")), + Optional.ofNullable( + testRailStatusMap.get(projectConfiguration.statusCanceled().toLowerCase().trim())) + .map(StatusDto::getId) + .orElseThrow( + () -> + new RuntimeException( + "Mapping for TestRail Cancelled Status " + + projectConfiguration.statusCanceled().toLowerCase().trim() + + " not found"))); + } + + /** + * Uploads a set of test results to TestRail + * + * @param resultsToUpload A set of results to upload + * @throws TestRailException when upload of batch failed + * @return set of invalid test case ID's + */ + public ImmutableSet uploadResults(@NonNull final Set resultsToUpload) + throws TestRailException { + + // validate case ID's first + paramValidator.validateTestCaseIds( + projectConfiguration.testRailProjectId(), + projectConfiguration.testRailSuiteId(), + resultsToUpload.stream() + .filter(Objects::nonNull) + .map(UploadResultDto::testCaseId) + .collect(ImmutableSet.toImmutableSet())); + + // upload results + final Table resultsToLogByRunNameAndCaseId = + HashBasedTable.create(); + resultsToUpload.forEach( + result -> + resultsToLogByRunNameAndCaseId.put( + projectConfiguration.testRailRunName(), + TestRailUtil.extractTestCaseId(result.testCaseId()), + new TestRailStatusComment( + testRailConfigExtended.getStatusMaps().getStatusMap().get(result.status()), + result.resultComment()))); + + var validateRequest = + new TestRailValidateRequest( + projectConfiguration.testRailProjectId(), + projectConfiguration.testRailSuiteId(), + projectConfiguration.testRailPlanName(), + testRailPlanId, + projectConfiguration.addAllTestsToPlan()); + return ImmutableSet.copyOf( + testRailResultLogger.execute(resultsToLogByRunNameAndCaseId, validateRequest)); + } + + /** + * DTO Used for uploading result to testrail + * + * @param testCaseId The test case id + * @param status The result status + * @param resultComment The comment + */ + public record UploadResultDto( + @EqualsAndHashCode.Include @NonNull String testCaseId, + @NonNull TestResultStatus status, + @NonNull String resultComment) {} + + /** + * The full TestRail project configuration + * + * @param testRailProjectId + * @param testRailSuiteId + * @param addAllTestsToPlan + * @param testRailPlanName + * @param testRailRunName + * @param statusPassed + * @param statusFailed + * @param statusSkipped + * @param statusError + * @param statusCanceled + */ + public record ProjectConfiguration( + long testRailProjectId, + long testRailSuiteId, + boolean addAllTestsToPlan, + @NonNull String testRailPlanName, + @NonNull String testRailRunName, + @NonNull String statusPassed, + @NonNull String statusFailed, + @NonNull String statusSkipped, + @NonNull String statusError, + @NonNull String statusCanceled) {} +} diff --git a/src/main/java/com/applause/auto/testrail/client/TestRailUtil.java b/src/main/java/com/applause/auto/testrail/client/TestRailUtil.java new file mode 100644 index 0000000..5e1c924 --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/TestRailUtil.java @@ -0,0 +1,177 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client; + +import com.applause.auto.testrail.client.enums.TestResultStatus; +import com.applause.auto.testrail.client.models.config.TestRailDto; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; +import lombok.NonNull; +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +/** A Utility class for interacting with TestRail */ +@UtilityClass +@Slf4j +public class TestRailUtil { + // TestRail default statuses. see http://docs.gurock.com/testrail-api2/reference-statuses + /** The TestRail passed status */ + public static final int TESTRAIL_PASSED_STATUS_ID = 1; + + /** The TestRail blocked status */ + public static final int TESTRAIL_BLOCKED_STATUS_ID = 2; + + /** The TestRail untested status */ + public static final int TESTRAIL_UNTESTED_STATUS_ID = 3; + + /** The TestRail retest status */ + public static final int TESTRAIL_RETEST_STATUS_ID = 4; + + /** The TestRail failed status */ + public static final int TESTRAIL_FAILED_STATUS_ID = 5; + + /** A default status mapping */ + public static final Map DEFAULT_STATUS_MAPPING; + + static { + DEFAULT_STATUS_MAPPING = new HashMap<>(); + DEFAULT_STATUS_MAPPING.put(TestResultStatus.PASSED, TESTRAIL_PASSED_STATUS_ID); + DEFAULT_STATUS_MAPPING.put(TestResultStatus.FAILED, TESTRAIL_FAILED_STATUS_ID); + DEFAULT_STATUS_MAPPING.put(TestResultStatus.SKIPPED, TESTRAIL_BLOCKED_STATUS_ID); + DEFAULT_STATUS_MAPPING.put(TestResultStatus.CANCELED, TESTRAIL_BLOCKED_STATUS_ID); + DEFAULT_STATUS_MAPPING.put(TestResultStatus.ERROR, TESTRAIL_BLOCKED_STATUS_ID); + } + + /** + * Helps to extract out the TestRail test case id from a String + * + * @param testCaseId The test case id string + * @return The parsed test case id + */ + public static long extractTestCaseId(@NonNull final String testCaseId) { + return Long.parseLong(testCaseId.replace("C", "")); + } + + /** + * Checks to make sure the test rails case id passed in is of a valid format + * + * @param testCaseId string to check + * @return true if valid (or blank/null), false if not + */ + public static boolean validateTestRailCaseId(final String testCaseId) { + if (StringUtils.isBlank(testCaseId)) { + return false; + } + // It's valid if it's a number or a number preceded by the character 'C' + return Pattern.matches("C?\\d+", testCaseId); + } + + /** + * Returns the customer's custom TestRail status mappings + * + * @param testRailDto company's TestRail account + * @return map from Applause TestResultStatus -> TestRail Test Result Status numeric ID + */ + public static Map buildCustomStatusMapping( + @NonNull final TestRailDto testRailDto) { + var mapping = new HashMap(); + mapping.put( + TestResultStatus.PASSED, + Optional.ofNullable(testRailDto.getMappedStatusPassed()) + .orElse(getDefaultStatus(TestResultStatus.PASSED))); + mapping.put( + TestResultStatus.FAILED, + Optional.ofNullable(testRailDto.getMappedStatusFailed()) + .orElse(getDefaultStatus(TestResultStatus.FAILED))); + mapping.put( + TestResultStatus.SKIPPED, + Optional.ofNullable(testRailDto.getMappedStatusSkipped()) + .orElse(getDefaultStatus(TestResultStatus.SKIPPED))); + mapping.put( + TestResultStatus.ERROR, + Optional.ofNullable(testRailDto.getMappedStatusError()) + .orElse(getDefaultStatus(TestResultStatus.ERROR))); + mapping.put( + TestResultStatus.CANCELED, + Optional.ofNullable(testRailDto.getMappedStatusCanceled()) + .orElse(getDefaultStatus(TestResultStatus.CANCELED))); + return mapping; + } + + /** + * Maps the Applause Test Result Status to TestRail status id. 1 = Passed 2 = Blocked 3 = Untested + * (not allowed when adding a result) 4 = Retest 5 = Failed ...plus optional custom per-company + * statuses. See "status_id" under Request Fields here + * + * @param status our status enum + * @param statusMapping optional custom applause -> TestRail result status mapping + * @return appropriate TestRail ID enum + */ + public static int getStatus( + @NonNull final TestResultStatus status, final Map statusMapping) { + if (null == statusMapping) { + log.warn("no custom status map passed in, going with default"); + return getDefaultStatus(status); + } + var customTestRailStatus = statusMapping.get(status); + if (null == customTestRailStatus) { + var defaultStatus = getDefaultStatus(status); + log.warn("no value found for {} in the mapping, using default {}", status, defaultStatus); + return defaultStatus; + } else { + log.debug( + "return custom TestRail status id {} for {}", customTestRailStatus, status.getValue()); + return customTestRailStatus; + } + } + + /** + * Returns the "default" mapped Applause -> TestRail test status + * + * @param status our status enum + * @return appropriate TestRail ID enum + */ + public static int getDefaultStatus(@NonNull final TestResultStatus status) { + var defaultStatus = TestRailUtil.DEFAULT_STATUS_MAPPING.get(status); + if (null == defaultStatus) { + throw new IllegalArgumentException("Status " + status + " does not have a default"); + } else { + return defaultStatus; + } + } + + /** + * Cleans Plan Name passed in from Jenkins into a TestRail friendly looking name. + * + * @param originalPlanName the original plan name passed in from Jenkins + * @return The cleaned testrail plan name + */ + public static String cleanTestRailPlanName(final String originalPlanName) { + // strip UTC from build timestamp that gets passed into the plan name + String planName = originalPlanName.replace(" UTC", ""); + + // replace underscores - only used for jenkins and reads better without + planName = planName.replace("_", " - "); + + return planName; + } +} diff --git a/src/main/java/com/applause/auto/testrail/client/enums/TestResultStatus.java b/src/main/java/com/applause/auto/testrail/client/enums/TestResultStatus.java new file mode 100644 index 0000000..cdc8cda --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/enums/TestResultStatus.java @@ -0,0 +1,55 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.enums; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import lombok.Getter; + +/** + * An internal status to be mapped to the TestRail status. TestRail has a number of internal or + * custom statuses that can change under the hood. These statuses provide a simple list of statuses + * that we can map later + */ +@Getter +public enum TestResultStatus { + /** The test has not been run yet */ + NOT_RUN("NOT_RUN"), + /** The test is currently in progress */ + IN_PROGRESS("IN_PROGRESS"), + /** The test has passed */ + PASSED("PASSED"), + /** The test has failed */ + FAILED("FAILED"), + /** The test has been skipped */ + SKIPPED("SKIPPED"), + /** The test has been canceled */ + CANCELED("CANCELED"), + /** The test has reached an error condition */ + ERROR("ERROR"); + + private final String value; + + TestResultStatus(final String value) { + this.value = value; + } + + /** A set of statuses considered "complete" */ + public static final ImmutableSet ALL_COMPLETE_STATUSES = + Sets.immutableEnumSet(PASSED, FAILED, SKIPPED, CANCELED, ERROR); +} diff --git a/src/main/java/com/applause/auto/testrail/client/errors/TestRailErrorStatus.java b/src/main/java/com/applause/auto/testrail/client/errors/TestRailErrorStatus.java new file mode 100644 index 0000000..9c56b28 --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/errors/TestRailErrorStatus.java @@ -0,0 +1,59 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.errors; + +import com.google.common.collect.Sets; +import java.util.Set; +import lombok.Getter; + +/** Common TestRail http failure mappings with related reasons. */ +@Getter +public enum TestRailErrorStatus { + /** No credentials are configured */ + NO_CREDENTIALS("TestRail credentials have not been configured."), + /** The status mappings do not line up with the actual TestRail statuses */ + STATUS_MAPPING("TestRail status mapping is misconfigured."), + /** An invalid case id was provided */ + CASE_ID("TestRail invalid case ID."), + /** TestRail rejected the request */ + BAD_REQUEST("TestRail rejected your request for invalid input."), + /** The TestRail rate limit was reached */ + HIT_RATE_LIMIT("TestRail rejected your request because the rate limit has been reached."), + /** Authentication with TestRail failed */ + AUTHENTICATION_FAILED("TestRail authentication failed."), + /** TestRail rejected access to the resource */ + ACCESS_DENIED("TestRail not allowing access to resource."), + /** The provided resource was not found */ + RESOURCE_NOT_FOUND("TestRail resource does not exist."), + /** An invalid http route was constructed */ + INVALID_ROUTE("The given TestRail endpoint does not exist."), + /** TestRail is performing maintenance */ + MAINTENANCE("TestRail is performing maintenance. Try again later."), + /** Another error occurred */ + UNKNOWN_ERROR("An unknown TestRail error has occurred."); + + /** A set of statuses that are considered non-blocking */ + public static final Set NON_BLOCKING_STATUSES = + Sets.immutableEnumSet(HIT_RATE_LIMIT, MAINTENANCE, UNKNOWN_ERROR); + + private final String message; + + TestRailErrorStatus(final String message) { + this.message = message; + } +} diff --git a/src/main/java/com/applause/auto/testrail/client/errors/TestRailException.java b/src/main/java/com/applause/auto/testrail/client/errors/TestRailException.java new file mode 100644 index 0000000..241b636 --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/errors/TestRailException.java @@ -0,0 +1,86 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.errors; + +import javax.annotation.Nullable; +import lombok.Getter; +import lombok.NonNull; + +/** An exception */ +@Getter +public class TestRailException extends Exception { + @NonNull private final TestRailErrorStatus status; + private boolean retryable = true; + + /** + * Creates a new TestRail exception from a TestRailErrorStatus + * + * @param status The TestRailErrorStatus + */ + public TestRailException(@NonNull final TestRailErrorStatus status) { + this(null, status, null); + } + + /** + * Creates a new TestRail exception from a TestRailErrorStatus + * + * @param status The TestRailErrorStatus + * @param cause The cause of the exception + */ + public TestRailException( + @NonNull final TestRailErrorStatus status, @Nullable final Exception cause) { + this(null, status, cause); + } + + /** + * Creates a new TestRail exception from a TestRailErrorStatus + * + * @param details Additional information about the error + * @param status The TestRailErrorStatus + */ + public TestRailException(@Nullable String details, @NonNull final TestRailErrorStatus status) { + this(details, status, null); + } + + /** + * Creates a new TestRail exception from a TestRailErrorStatus + * + * @param details Additional information about the error + * @param status The TestRailErrorStatus + * @param cause The cause of the exception + */ + public TestRailException( + @Nullable final String details, + @NonNull final TestRailErrorStatus status, + @Nullable final Exception cause) { + super(status.getMessage() + (details != null ? " Details: " + details : ""), cause); + this.status = status; + } + + /** + * Marks whether the exception is considered retryable + * + * @param retryable True if the action can be retried + * @return The TestRailException + */ + @NonNull + public TestRailException setRetryable(boolean retryable) { + this.retryable = retryable; + return this; + } +} diff --git a/src/main/java/com/applause/auto/testrail/client/interceptors/GenericErrorInterceptor.java b/src/main/java/com/applause/auto/testrail/client/interceptors/GenericErrorInterceptor.java new file mode 100644 index 0000000..d04f417 --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/interceptors/GenericErrorInterceptor.java @@ -0,0 +1,76 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.interceptors; + +import com.google.common.collect.ImmutableSet; +import java.io.IOException; +import java.util.Set; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import okhttp3.Interceptor; +import okhttp3.Request; +import org.apache.commons.lang3.StringUtils; + +/** A Generic OkHttp error interceptor for the TestRail API */ +@Slf4j +public class GenericErrorInterceptor implements Interceptor { + private static final Long BYTES_TO_PEEK = 20_000L; + private static final Set DEFAULT_STATUS_CODES_TO_EXCLUDE = ImmutableSet.of(); + + private final Set excludedStatusCodes; + + /** Set up a new Error Interceptor using the default status codes */ + public GenericErrorInterceptor() { + this(DEFAULT_STATUS_CODES_TO_EXCLUDE); + } + + /** + * Note: the excludedStatusCodes overrides any default status codes that exist + * + * @param excludedStatusCodes - The interceptor will not log any status codes in the set + */ + public GenericErrorInterceptor(final @NonNull Set excludedStatusCodes) { + log.trace( + "Excluding Error statuses from reporting: " + StringUtils.join(excludedStatusCodes, ",")); + this.excludedStatusCodes = excludedStatusCodes; + } + + @NonNull + @Override + public okhttp3.Response intercept(final @NonNull Interceptor.Chain chain) throws IOException { + final Request request = chain.request(); + final okhttp3.Response response = chain.proceed(request); + + if (!response.isSuccessful()) { + log.trace("Http response not successful."); + final int responseCode = response.code(); + if (!excludedStatusCodes.contains(responseCode)) { + log.info( + "Failed call with response status: '{}' to host: '{}' with path: '{}' and response body '{}'", + responseCode, + request.url().url().getHost(), + request.url().url().getPath(), + response.peekBody(BYTES_TO_PEEK).string()); + } + + return response; + } + + return response; + } +} diff --git a/src/main/java/com/applause/auto/testrail/client/interceptors/HeadersInterceptor.java b/src/main/java/com/applause/auto/testrail/client/interceptors/HeadersInterceptor.java new file mode 100644 index 0000000..ffd0ca6 --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/interceptors/HeadersInterceptor.java @@ -0,0 +1,57 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.interceptors; + +import com.google.common.net.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import java.io.IOException; +import lombok.AllArgsConstructor; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import okhttp3.Credentials; +import okhttp3.Interceptor; +import okhttp3.Response; + +/** OkHttp Interceptor for adding common Http Headers to the request */ +@AllArgsConstructor +@Slf4j +public class HeadersInterceptor implements Interceptor { + private final String userName; + private final String apiKey; + + /** + * Intercepts the okhttp request and adds common headers + * + * @param chain The Http Chain + * @return The response + * @throws IOException If I/O Fails + */ + @Override + public @NonNull Response intercept(final Chain chain) throws IOException { + log.trace("Adding headers to Http client"); + final var request = chain.request(); + final var newRequest = + request + .newBuilder() + .addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON) + .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) + .addHeader(HttpHeaders.AUTHORIZATION, Credentials.basic(userName, apiKey)) + .build(); + return chain.proceed(newRequest); + } +} diff --git a/src/main/java/com/applause/auto/testrail/client/interceptors/UrlInterceptor.java b/src/main/java/com/applause/auto/testrail/client/interceptors/UrlInterceptor.java new file mode 100644 index 0000000..4b406c1 --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/interceptors/UrlInterceptor.java @@ -0,0 +1,52 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.interceptors; + +import java.io.IOException; +import lombok.extern.slf4j.Slf4j; +import okhttp3.Interceptor; +import okhttp3.Response; +import org.jetbrains.annotations.NotNull; + +/** OKHttp Interception for updating request URLs to match the TestRail api format */ +@Slf4j +public class UrlInterceptor implements Interceptor { + + /** + * Overrides the request url to use query based routing + * + * @param chain The Http Chain + * @return The response + * @throws IOException If request fails + */ + @Override + public @NotNull Response intercept(final Chain chain) throws IOException { + String basePath = "/index.php"; + String baseQueryPath = "/api/v2"; + log.trace( + "Updating base url to expected path containing " + basePath + " and " + baseQueryPath); + final var request = chain.request(); + final var newUrl = request.url().newBuilder(); + // in URL everything after host is "file name", this includes query params and path + final var path = request.url().url().getPath(); + final var query = request.url().url().getQuery(); + newUrl.encodedPath(basePath).query(baseQueryPath + path + "&" + query); + final var newRequest = request.newBuilder().url(newUrl.build()).build(); + return chain.proceed(newRequest); + } +} diff --git a/src/main/java/com/applause/auto/testrail/client/models/applause/AcquireTestPlanDto.java b/src/main/java/com/applause/auto/testrail/client/models/applause/AcquireTestPlanDto.java new file mode 100644 index 0000000..ed72e5e --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/applause/AcquireTestPlanDto.java @@ -0,0 +1,31 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.applause; + +import com.applause.auto.testrail.client.models.config.TestRailDto; + +/** + * DTO that can be passed to the Applause Services to acquire a TestPlan + * + * @param projectId The ID of the TestRail Project + * @param planId The ID of the plan + * @param testPlanName The Name of the plan + * @param config The TesRail config + */ +public record AcquireTestPlanDto( + Long projectId, Long planId, String testPlanName, TestRailDto config) {} diff --git a/src/main/java/com/applause/auto/testrail/client/models/config/TestRailConfig.java b/src/main/java/com/applause/auto/testrail/client/models/config/TestRailConfig.java new file mode 100644 index 0000000..81ea8b4 --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/config/TestRailConfig.java @@ -0,0 +1,46 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.config; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NonNull; +import lombok.experimental.SuperBuilder; + +/** The TestRail config class */ +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class TestRailConfig extends TestRailCredentials { + @NonNull private final String url; + + /** + * Converts this config to an extended config + * + * @param statusMaps The status mapping + * @return The extended config + */ + public TestRailConfigExtended toExtended(@NonNull final TestRailStatusMaps statusMaps) { + return TestRailConfigExtended.builder() + .email(this.getEmail()) + .apiKey(this.getApiKey()) + .url(this.getUrl()) + .statusMaps(statusMaps) + .build(); + } +} diff --git a/src/main/java/com/applause/auto/testrail/client/models/config/TestRailConfigExtended.java b/src/main/java/com/applause/auto/testrail/client/models/config/TestRailConfigExtended.java new file mode 100644 index 0000000..a5f9712 --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/config/TestRailConfigExtended.java @@ -0,0 +1,31 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.config; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NonNull; +import lombok.experimental.SuperBuilder; + +/** TestRail's configuration with status mappings */ +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public class TestRailConfigExtended extends TestRailConfig { + @NonNull private final TestRailStatusMaps statusMaps; +} diff --git a/src/main/java/com/applause/auto/testrail/client/models/config/TestRailCredentials.java b/src/main/java/com/applause/auto/testrail/client/models/config/TestRailCredentials.java new file mode 100644 index 0000000..a7de6dc --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/config/TestRailCredentials.java @@ -0,0 +1,30 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.config; + +import lombok.Data; +import lombok.NonNull; +import lombok.experimental.SuperBuilder; + +/** TestRail credential configuration */ +@Data +@SuperBuilder +public class TestRailCredentials { + @NonNull private final String email; + @NonNull private final String apiKey; +} diff --git a/src/main/java/com/applause/auto/testrail/client/models/config/TestRailDto.java b/src/main/java/com/applause/auto/testrail/client/models/config/TestRailDto.java new file mode 100644 index 0000000..af4c554 --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/config/TestRailDto.java @@ -0,0 +1,42 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.config; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** The Full TestRail Config */ +@Accessors(chain = true) +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TestRailDto { + private long id; + private Long companyId; + private String email; + private String apiKey; + private String url; + private Integer mappedStatusPassed; + private Integer mappedStatusFailed; + private Integer mappedStatusSkipped; + private Integer mappedStatusError; + private Integer mappedStatusCanceled; + private boolean badMappingStatus; +} diff --git a/src/main/java/com/applause/auto/testrail/client/models/config/TestRailStatusMaps.java b/src/main/java/com/applause/auto/testrail/client/models/config/TestRailStatusMaps.java new file mode 100644 index 0000000..076eebd --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/config/TestRailStatusMaps.java @@ -0,0 +1,112 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.config; + +import com.applause.auto.testrail.client.TestRailUtil; +import com.applause.auto.testrail.client.enums.TestResultStatus; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import lombok.Getter; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +/** The Integer values to map to the TestRail StatusDto keys */ +@Getter +@Slf4j +public class TestRailStatusMaps { + private final Map statusMap; + private final int mappedStatusPassed; + private final int mappedStatusFailed; + private final int mappedStatusSkipped; + private final int mappedStatusError; + private final int mappedStatusCanceled; + + /** + * Constructs a new Status map from a TestRail config DTO + * + * @param testRailDto The TestRail config + */ + public TestRailStatusMaps(@NonNull TestRailDto testRailDto) { + // make sure stored map is immutable since this is data class + statusMap = ImmutableMap.copyOf(TestRailUtil.buildCustomStatusMapping(testRailDto)); + this.mappedStatusPassed = testRailDto.getMappedStatusPassed(); + this.mappedStatusSkipped = testRailDto.getMappedStatusSkipped(); + this.mappedStatusFailed = testRailDto.getMappedStatusFailed(); + this.mappedStatusCanceled = testRailDto.getMappedStatusCanceled(); + this.mappedStatusError = testRailDto.getMappedStatusError(); + } + + /** + * Construct a new status mapping + * + * @param mappedStatusPassed The status to map passed result to + * @param mappedStatusFailed The status to map failed result to + * @param mappedStatusSkipped The status to map shipped result to + * @param mappedStatusError The status to map error result to + * @param mappedStatusCanceled The status to map canceled result to + */ + public TestRailStatusMaps( + int mappedStatusPassed, + int mappedStatusFailed, + int mappedStatusSkipped, + int mappedStatusError, + int mappedStatusCanceled) { + this.mappedStatusPassed = mappedStatusPassed; + this.mappedStatusFailed = mappedStatusFailed; + this.mappedStatusSkipped = mappedStatusSkipped; + this.mappedStatusError = mappedStatusError; + this.mappedStatusCanceled = mappedStatusCanceled; + statusMap = + ImmutableMap.builder() + .put(TestResultStatus.PASSED, mappedStatusPassed) + .put(TestResultStatus.FAILED, mappedStatusFailed) + .put(TestResultStatus.SKIPPED, mappedStatusSkipped) + .put(TestResultStatus.ERROR, mappedStatusError) + .put(TestResultStatus.CANCELED, mappedStatusCanceled) + .build(); + var duplicates = findDuplicates(statusMap.values()); + log.warn( + "duplicate status mappings found for TestRail statuses " + + Joiner.on(", ").join(duplicates) + + " If this was not intended, make sure each TestRail status integer maps to a unique test result status"); + } + + private static ImmutableSet findDuplicates(Collection collection) { + var uniques = new HashSet<>(); + return collection.stream().filter(e -> !uniques.add(e)).collect(ImmutableSet.toImmutableSet()); + } + + /** + * Gets the set of testrail status ids included in this map + * + * @return The set of testrail status ids + */ + public Set getTestRailStatusIds() { + return Set.of( + this.mappedStatusPassed, + this.mappedStatusFailed, + this.mappedStatusSkipped, + this.mappedStatusCanceled, + this.mappedStatusError); + } +} diff --git a/src/main/java/com/applause/auto/testrail/client/models/internal/TestRailRunsAndInvalidCases.java b/src/main/java/com/applause/auto/testrail/client/models/internal/TestRailRunsAndInvalidCases.java new file mode 100644 index 0000000..89f2372 --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/internal/TestRailRunsAndInvalidCases.java @@ -0,0 +1,33 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.internal; + +import com.applause.auto.testrail.client.models.testrail.TestRunDto; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import lombok.Data; +import lombok.NonNull; + +/** A map of TestRail Runs and invalid test case ids */ +@Data +public class TestRailRunsAndInvalidCases { + @NonNull private Map runDtosByName = new HashMap<>(); + @NonNull private Set invalidCaseIds = new HashSet<>(); +} diff --git a/src/main/java/com/applause/auto/testrail/client/models/internal/TestRailStatusComment.java b/src/main/java/com/applause/auto/testrail/client/models/internal/TestRailStatusComment.java new file mode 100644 index 0000000..0af81dc --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/internal/TestRailStatusComment.java @@ -0,0 +1,23 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.internal; + +import lombok.NonNull; + +/** Named pair class for TestRail status and comment pairs */ +public record TestRailStatusComment(@NonNull Integer statusId, @NonNull String comment) {} diff --git a/src/main/java/com/applause/auto/testrail/client/models/internal/TestRailValidateRequest.java b/src/main/java/com/applause/auto/testrail/client/models/internal/TestRailValidateRequest.java new file mode 100644 index 0000000..36c0d4a --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/internal/TestRailValidateRequest.java @@ -0,0 +1,28 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.internal; + +import lombok.NonNull; + +/** A validation request for TestRail values */ +public record TestRailValidateRequest( + @NonNull Long projectId, + @NonNull Long suiteId, + @NonNull String planName, + Long planId, + boolean includeAll) {} diff --git a/src/main/java/com/applause/auto/testrail/client/models/testrail/AddPlanDto.java b/src/main/java/com/applause/auto/testrail/client/models/testrail/AddPlanDto.java new file mode 100644 index 0000000..e1ed3cc --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/testrail/AddPlanDto.java @@ -0,0 +1,41 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.testrail; + +import java.util.List; +import lombok.Builder; +import lombok.Data; +import lombok.NonNull; + +/** DTO for adding a new plan */ +@Data +@Builder +public class AddPlanDto { + + /** The name of the test plan (required) */ + @NonNull private final String name; + + /** The description of the test plan */ + private final String description; + + /** The ID of the milestone to link to the test plan */ + private final Long milestoneId; + + /** An array of objects describing the test runs of the plan */ + private final List entries; +} diff --git a/src/main/java/com/applause/auto/testrail/client/models/testrail/AddPlanEntryDto.java b/src/main/java/com/applause/auto/testrail/client/models/testrail/AddPlanEntryDto.java new file mode 100644 index 0000000..da3553e --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/testrail/AddPlanEntryDto.java @@ -0,0 +1,60 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.testrail; + +import com.google.gson.annotations.SerializedName; +import java.util.List; +import java.util.Set; +import lombok.Builder; +import lombok.Data; + +/** DTO for adding a new Plan Entry */ +@Data +@Builder +public class AddPlanEntryDto { + + /** The ID of the test suite for the test run(s) (required) */ + private final long suiteId; + + /** The name of the test run(s) */ + private final String name; + + /** The description of the test run(s) (requires TestRail 5.2 or later) */ + private final String description; + + /** The ID of the user the test run(s) should be assigned to */ + @SerializedName("assignedto_id") + private final Long assignedToId; + + /** + * True for including all test cases of the test suite and false for a custom case selection + * (default: true) + */ + private final Boolean includeAll; + + /** An array of case IDs for the custom case selection */ + private final Set caseIds; + + /** + * An array of configuration IDs used for the test runs of the test plan entry (requires TestRail + * 3.1 or later) + */ + private final List configIds; + + private final List runs; +} diff --git a/src/main/java/com/applause/auto/testrail/client/models/testrail/AddTestResultForCaseDto.java b/src/main/java/com/applause/auto/testrail/client/models/testrail/AddTestResultForCaseDto.java new file mode 100644 index 0000000..5b06ec6 --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/testrail/AddTestResultForCaseDto.java @@ -0,0 +1,49 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.testrail; + +import com.google.gson.annotations.SerializedName; +import lombok.Builder; +import lombok.Data; + +/** DTO for adding a new TestResult for a test case */ +@Data +@Builder +public class AddTestResultForCaseDto { + /** The ID of the test case */ + private final Long caseId; + + /** The ID of the test status */ + private final Integer statusId; + + /** The comment / description for the test result */ + private final String comment; + + /** The version or build you tested against */ + private final String version; + + /** The time it took to execute the test, e.g. "30s" or "1m 45s". */ + private final String elapsed; + + /** A comma-separated list of defects to link to the test result */ + private final String defects; + + /** The ID of a user the test should be assigned to */ + @SerializedName("assignedto_id") + private final Long assignedToId; +} diff --git a/src/main/java/com/applause/auto/testrail/client/models/testrail/AddTestResultsForCaseDto.java b/src/main/java/com/applause/auto/testrail/client/models/testrail/AddTestResultsForCaseDto.java new file mode 100644 index 0000000..8724f57 --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/testrail/AddTestResultsForCaseDto.java @@ -0,0 +1,39 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.testrail; + +import java.util.ArrayList; +import java.util.List; +import lombok.Data; + +/** DTO for adding a set of test results */ +@Data +public class AddTestResultsForCaseDto { + + // All results are wrapped in this object + private final List results = new ArrayList<>(); + + /** + * Adds a new TestResult for a test case + * + * @param res The result to add for the test case + */ + public void add(final AddTestResultForCaseDto res) { + this.results.add(res); + } +} diff --git a/src/main/java/com/applause/auto/testrail/client/models/testrail/CustomStepDto.java b/src/main/java/com/applause/auto/testrail/client/models/testrail/CustomStepDto.java new file mode 100644 index 0000000..3616676 --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/testrail/CustomStepDto.java @@ -0,0 +1,26 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.testrail; + +/** + * A DTO representing a custom step + * + * @param content + * @param expected + */ +public record CustomStepDto(String content, String expected) {} diff --git a/src/main/java/com/applause/auto/testrail/client/models/testrail/PaginatedBulkCaseDto.java b/src/main/java/com/applause/auto/testrail/client/models/testrail/PaginatedBulkCaseDto.java new file mode 100644 index 0000000..f085eda --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/testrail/PaginatedBulkCaseDto.java @@ -0,0 +1,32 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.testrail; + +import java.util.List; + +/** + * A DTO representing a paginated list of test cases + * + * @param offset The page offset + * @param limit The max number of results + * @param size The size of results + * @param _links The pagination links + * @param cases The list of testcases + */ +public record PaginatedBulkCaseDto( + int offset, int limit, int size, PaginatedLinkDto _links, List cases) {} diff --git a/src/main/java/com/applause/auto/testrail/client/models/testrail/PaginatedBulkPlanDto.java b/src/main/java/com/applause/auto/testrail/client/models/testrail/PaginatedBulkPlanDto.java new file mode 100644 index 0000000..9802a4d --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/testrail/PaginatedBulkPlanDto.java @@ -0,0 +1,32 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.testrail; + +import java.util.List; + +/** + * A DTO representing a paginated list of test plans + * + * @param offset The page offset + * @param limit The max number of results + * @param size The size of results + * @param _links The pagination links + * @param plans The list of test plans + */ +public record PaginatedBulkPlanDto( + int offset, int limit, int size, PaginatedLinkDto _links, List plans) {} diff --git a/src/main/java/com/applause/auto/testrail/client/models/testrail/PaginatedBulkTestDto.java b/src/main/java/com/applause/auto/testrail/client/models/testrail/PaginatedBulkTestDto.java new file mode 100644 index 0000000..f470068 --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/testrail/PaginatedBulkTestDto.java @@ -0,0 +1,32 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.testrail; + +import java.util.List; + +/** + * A Paginated Test Case response + * + * @param offset The page offset + * @param limit The page size limit + * @param size The total number of test cases + * @param _links A pagination link + * @param tests The tests in this page + */ +public record PaginatedBulkTestDto( + int offset, int limit, int size, PaginatedLinkDto _links, List tests) {} diff --git a/src/main/java/com/applause/auto/testrail/client/models/testrail/PaginatedLinkDto.java b/src/main/java/com/applause/auto/testrail/client/models/testrail/PaginatedLinkDto.java new file mode 100644 index 0000000..bca5761 --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/testrail/PaginatedLinkDto.java @@ -0,0 +1,26 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.testrail; + +/** + * A link attached to paginated results pointing to the next and previous page + * + * @param next The next page + * @param prev The previous page + */ +public record PaginatedLinkDto(String next, String prev) {} diff --git a/src/main/java/com/applause/auto/testrail/client/models/testrail/PlanDto.java b/src/main/java/com/applause/auto/testrail/client/models/testrail/PlanDto.java new file mode 100644 index 0000000..2a9fe15 --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/testrail/PlanDto.java @@ -0,0 +1,109 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.testrail; + +import com.google.gson.annotations.SerializedName; +import java.util.List; +import lombok.Builder; +import lombok.Data; + +/** A DTO describing a test plan */ +@Data +@Builder +public class PlanDto { + + /** The ID of the user the entire test plan is assigned to */ + @SerializedName("assignedto_id") + private final Long assignedToId; + + /** The amount of tests in the test plan marked as blocked */ + private final Long blockedCount; + + /** The date/time when the test plan was closed (as UNIX timestamp) */ + private final String completedOn; + + /** The ID of the user who created the test plan */ + private final Long createdBy; + + /** The date/time when the test plan was created (as UNIX timestamp) */ + private final Long createdOn; + + /** The amount of tests in the test plan with the respective custom status */ + @SerializedName("custom_status1_count") + private final Long customStatus1Count; + + /** The amount of tests in the test plan with the respective custom status */ + @SerializedName("custom_status2_count") + private final Long customStatus2Count; + + /** The amount of tests in the test plan with the respective custom status */ + @SerializedName("custom_status3_count") + private final Long customStatus3Count; + + /** The amount of tests in the test plan with the respective custom status */ + @SerializedName("custom_status4_count") + private final Long customStatus4Count; + + /** The amount of tests in the test plan with the respective custom status */ + @SerializedName("custom_status5_count") + private final Long customStatus5Count; + + /** The amount of tests in the test plan with the respective custom status */ + @SerializedName("custom_status6_count") + private final Long customStatus6Count; + + /** The amount of tests in the test plan with the respective custom status */ + @SerializedName("custom_status7_count") + private final Long customStatus7Count; + + /** The description of the test plan */ + private final String description; + + /** An array of 'entries', i.e. group of test runs */ + private final List entries; + + /** The amount of tests in the test plan marked as failed */ + private final Long failedCount; + + /** The unique ID of the test plan */ + private final Long id; + + /** True if the test plan was closed and false otherwise */ + private final Boolean isCompleted; + + /** The ID of the milestone this test plan belongs to */ + private final Long milestoneId; + + /** The name of the test plan */ + private final String name; + + /** The amount of tests in the test plan marked as passed */ + private final Long passedCount; + + /** The ID of the project this test plan belongs to */ + private final Long projectId; + + /** The amount of tests in the test plan marked as retest */ + private final Long retestCount; + + /** The amount of tests in the test plan marked as untested */ + private final Long untestedCount; + + /** The address/URL of the test plan in the user interface */ + private final String url; +} diff --git a/src/main/java/com/applause/auto/testrail/client/models/testrail/PlanEntryDto.java b/src/main/java/com/applause/auto/testrail/client/models/testrail/PlanEntryDto.java new file mode 100644 index 0000000..28b9ad3 --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/testrail/PlanEntryDto.java @@ -0,0 +1,40 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.testrail; + +import java.util.List; +import lombok.Builder; +import lombok.Data; + +/** A DTO describing a TestRail Test Plan Entry */ +@Data +@Builder +public class PlanEntryDto { + + /** plan entry id */ + private final String id; + + /** plan entry name */ + private final String name; + + /** list of runs */ + private final List runs; + + /** Test suite id */ + private final Long suiteId; +} diff --git a/src/main/java/com/applause/auto/testrail/client/models/testrail/PlanEntryRunDto.java b/src/main/java/com/applause/auto/testrail/client/models/testrail/PlanEntryRunDto.java new file mode 100644 index 0000000..fee1e6a --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/testrail/PlanEntryRunDto.java @@ -0,0 +1,38 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.testrail; + +import com.google.gson.annotations.SerializedName; +import java.util.List; +import lombok.Builder; +import lombok.Data; + +/** A DTO for adding a plan entry run */ +@Data +@Builder +public class PlanEntryRunDto { + + @SerializedName("assignedto_id") + private final Long assignedToId; + + private final List configIds; + + private final List caseIds; + + private final Boolean includeAll; +} diff --git a/src/main/java/com/applause/auto/testrail/client/models/testrail/ProjectDto.java b/src/main/java/com/applause/auto/testrail/client/models/testrail/ProjectDto.java new file mode 100644 index 0000000..6b3592f --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/testrail/ProjectDto.java @@ -0,0 +1,47 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.testrail; + +import com.google.gson.annotations.SerializedName; +import lombok.Builder; +import lombok.Data; + +/** A DTO describing a TestRail Project */ +@Data +@Builder +public class ProjectDto { + private int id; + + private String announcement; + + @SerializedName("completed_on") + private int completedOn; + + @SerializedName("is_completed") + private boolean isCompleted; + + private String name; + + @SerializedName("show_announcement") + private boolean showAnnouncement; + + @SerializedName("suite_mode") + private int suiteMode; + + private String url; +} diff --git a/src/main/java/com/applause/auto/testrail/client/models/testrail/StatusDto.java b/src/main/java/com/applause/auto/testrail/client/models/testrail/StatusDto.java new file mode 100644 index 0000000..f7ced6b --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/testrail/StatusDto.java @@ -0,0 +1,58 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.testrail; + +import com.google.gson.annotations.SerializedName; +import lombok.Builder; +import lombok.Data; +import lombok.NonNull; + +/** + * A DTO describing a TestRail result status + * + * @see Reference docs + */ +@Data +@Builder +public class StatusDto { + // color attributes + @SerializedName("color_bright") + private final Long colorBright; + + @SerializedName("color_dark") + private final Long colorDark; + + @SerializedName("color_medium") + private final Long colorMedium; + + // other internal flags + @SerializedName("is_final") + private final Boolean isFinal; + + @SerializedName("is_system") + private final Boolean isSystem; + + @SerializedName("is_untested") + private final Boolean isUntested; + + @NonNull private final Integer id; + // display value to be used by the UI + @NonNull private final String label; + // internal name + @NonNull private final String name; +} diff --git a/src/main/java/com/applause/auto/testrail/client/models/testrail/TestCaseDto.java b/src/main/java/com/applause/auto/testrail/client/models/testrail/TestCaseDto.java new file mode 100644 index 0000000..92f2c6f --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/testrail/TestCaseDto.java @@ -0,0 +1,29 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.testrail; + +import lombok.Data; + +/** A subset of the fields returned by the get_case endpoint in TestRail */ +@Data +public class TestCaseDto { + private long id; + + /** The title of the test case */ + private String title; +} diff --git a/src/main/java/com/applause/auto/testrail/client/models/testrail/TestDto.java b/src/main/java/com/applause/auto/testrail/client/models/testrail/TestDto.java new file mode 100644 index 0000000..169d473 --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/testrail/TestDto.java @@ -0,0 +1,84 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.testrail; + +import com.google.gson.annotations.SerializedName; +import java.util.List; +import lombok.Builder; +import lombok.Data; + +/** A DTO representing a test case */ +@Data +@Builder +public class TestDto { + + /** The ID of the user the test is assigned to */ + @SerializedName("assignedto_id") + private final Long assignedToId; + + /** The ID of the related test case */ + private final Long caseId; + + /** The estimate of the related test case, e.g. "30s" or "1m 45s" */ + private final String estimate; + + /** The estimate forecast of the related test case, e.g. "30s" or "1m 45s" */ + private final String estimateForecast; + + /** The unique ID of the test */ + private final Long id; + + /** The ID of the milestone that is linked to the test case */ + private final Long milestoneId; + + /** The ID of the priority that is linked to the test case */ + private final Long priorityId; + + /** A comma-separated list of references/requirements that are linked to the test case */ + private final String refs; + + /** The ID of the test run the test belongs to */ + private final Long runId; + + /** The ID of the current status of the test, also see get_statuses */ + private final Integer statusId; + + /** The title of the related test case */ + private final String title; + + /** The ID of the test case type that is linked to the test case */ + private final Long typeId; + + /** + * Custom fields of test cases are included in the response and use their system name prefixed + * with 'custom' + */ + private final String customExpected; + + /** + * Custom fields of test cases are included in the response and use their system name prefixed + * with 'custom' + */ + private final String customPreconds; + + /** + * Custom fields of test cases are included in the response and use their system name prefixed + * with 'custom' + */ + private final List customStepsSeparated; +} diff --git a/src/main/java/com/applause/auto/testrail/client/models/testrail/TestResultDto.java b/src/main/java/com/applause/auto/testrail/client/models/testrail/TestResultDto.java new file mode 100644 index 0000000..8c20bfd --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/testrail/TestResultDto.java @@ -0,0 +1,59 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.testrail; + +import com.google.gson.annotations.SerializedName; +import lombok.Builder; +import lombok.Data; + +/** A DTO representing a TestResult */ +@Data +@Builder +public class TestResultDto { + + /** The ID of the assignee (user) of the test result */ + @SerializedName("assignedto_id") + private final Long assignedToId; + + /** The comment or error message of the test result */ + private final String comment; + + /** The ID of the user who created the test result */ + private final Long createdBy; + + /** The date/time when the test result was created (as UNIX timestamp) */ + private final String createdOn; + + /** A comma-separated list of defects linked to the test result */ + private final String defects; + + /** The amount of time it took to execute the test (e.g. "1m" or "2m 30s") */ + private final String elapsed; + + /** The unique ID of the test result */ + private final Long id; + + /** The status of the test result, e.g. passed or failed, also see get_statuses */ + private final Integer statusId; + + /** The ID of the test this test result belongs to */ + private final Long testId; + + /** The (build) version the test was executed against */ + private final String version; +} diff --git a/src/main/java/com/applause/auto/testrail/client/models/testrail/TestRunDto.java b/src/main/java/com/applause/auto/testrail/client/models/testrail/TestRunDto.java new file mode 100644 index 0000000..8367215 --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/testrail/TestRunDto.java @@ -0,0 +1,97 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.testrail; + +import com.google.gson.annotations.SerializedName; +import java.util.List; +import lombok.Builder; +import lombok.Data; + +/** A DTO representing a TestRail Test Run */ +@Data +@Builder +public class TestRunDto { + + @SerializedName("assignedto_id") + private final Long assignedToId; + + private final Long blockedCount; + + private final String completedOn; + + /** CSV format */ + private final String config; + + private final List configIds; + + @SerializedName("custom_status1_count") + private final Long customStatus1Count; + + @SerializedName("custom_status2_count") + private final Long customStatus2Count; + + @SerializedName("custom_status3_count") + private final Long customStatus3Count; + + @SerializedName("custom_status4_count") + private final Long customStatus4Count; + + @SerializedName("custom_status5_count") + private final Long customStatus5Count; + + @SerializedName("custom_status6_count") + private final Long customStatus6Count; + + @SerializedName("custom_status7_count") + private final Long customStatus7Count; + + private final String description; + + private final String entryId; + + private final Long entryIndex; + + private final Long failedCount; + + private final Long id; + + private final Boolean includeAll; + + private final Boolean isCompleted; + + private final Long milestoneId; + + private final String name; + + private final Long passedCount; + + private final Long planId; + + private final Long projectId; + + private final Long createdOn; + + private final Long retestCount; + + private final Long suiteId; + + private final Long untestedCount; + + /** the test run page URL */ + private final String url; +} diff --git a/src/main/java/com/applause/auto/testrail/client/models/testrail/TestSuiteDto.java b/src/main/java/com/applause/auto/testrail/client/models/testrail/TestSuiteDto.java new file mode 100644 index 0000000..c677781 --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/testrail/TestSuiteDto.java @@ -0,0 +1,52 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.testrail; + +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +/** A DTO Representing a TestRail Test Suite */ +@Data +@Builder +@AllArgsConstructor +public class TestSuiteDto { + private int id; + + private String description; + + @SerializedName("completed_on") + private int completedOn; + + @SerializedName("is_baseline") + private boolean isBaseline; + + @SerializedName("is_completed") + private boolean isCompleted; + + @SerializedName("is_master") + private boolean isMaster; + + private String name; + + @SerializedName("project_id") + private int projectId; + + private String url; +} diff --git a/src/main/java/com/applause/auto/testrail/client/models/testrail/UpdatePlanEntryDto.java b/src/main/java/com/applause/auto/testrail/client/models/testrail/UpdatePlanEntryDto.java new file mode 100644 index 0000000..6b216b8 --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/testrail/UpdatePlanEntryDto.java @@ -0,0 +1,46 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.testrail; + +import java.util.Set; +import lombok.Builder; +import lombok.Data; + +/** A DTO used for updating a TestRail Plan Entry */ +@Data +@Builder +public class UpdatePlanEntryDto { + + /** test run name(s) */ + private final String name; + + /** description of the test run(s) */ + private final String description; + + /** The ID of the user the test run(s) should be assigned to */ + private final Long assignedToId; + + /** + * True for including all test cases of the test suite and false for a custom case selection + * (default: true) + */ + private final Boolean includeAll; + + /** An array of case IDs for the custom case selection */ + private final Set caseIds; +} diff --git a/src/main/java/com/applause/auto/testrail/client/models/testrail/UpdateTestResultDto.java b/src/main/java/com/applause/auto/testrail/client/models/testrail/UpdateTestResultDto.java new file mode 100644 index 0000000..8bb9d7f --- /dev/null +++ b/src/main/java/com/applause/auto/testrail/client/models/testrail/UpdateTestResultDto.java @@ -0,0 +1,47 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client.models.testrail; + +import com.google.gson.annotations.SerializedName; +import lombok.Builder; +import lombok.Data; + +/** A DTO used for updating a TestRail Test Result */ +@Data +@Builder +public class UpdateTestResultDto { + + /** The ID of the test status */ + private final Integer statusId; + + /** The comment / description for the test result */ + private final String comment; + + /** The version or build you tested against */ + private final String version; + + /** The time it took to execute the test, e.g. "30s" or "1m 45s". */ + private final String elapsed; + + /** A comma-separated list of defects to link to the test result */ + private final String defects; + + /** The ID of a user the test should be assigned to */ + @SerializedName("assignedto_id") + private final Long assignedToId; +} diff --git a/src/test/java/com/applause/auto/testrail/client/TestRailClientTest.java b/src/test/java/com/applause/auto/testrail/client/TestRailClientTest.java new file mode 100644 index 0000000..fb5cde9 --- /dev/null +++ b/src/test/java/com/applause/auto/testrail/client/TestRailClientTest.java @@ -0,0 +1,428 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import com.applause.auto.testrail.client.errors.TestRailErrorStatus; +import com.applause.auto.testrail.client.errors.TestRailException; +import com.applause.auto.testrail.client.models.testrail.*; +import jakarta.ws.rs.core.Response.Status; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import lombok.SneakyThrows; +import okhttp3.OkHttpClient; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import retrofit2.Response; + +@SuppressWarnings({"unchecked", "rawtypes"}) +public class TestRailClientTest { + private static final TestRailApi testRailApi = mock(TestRailApi.class); + private static final CompletableFuture mockFuture = mock(CompletableFuture.class); + private static final Response mockResponse = mock(Response.class); + + private static final Set setOfZero = Set.of(0L); + + private static TestRailClient client; + + @BeforeAll + static void beforeAll() { + OkHttpClient httpClient = mock(OkHttpClient.class); + var clientBuilder = mock(OkHttpClient.Builder.class); + when(clientBuilder.addInterceptor(any())).thenReturn(clientBuilder); + when(clientBuilder.build()).thenReturn(httpClient); + when(httpClient.newBuilder()).thenReturn(clientBuilder); + } + + @BeforeEach + public void setup() { + reset(testRailApi, mockResponse, mockFuture); + client = spy(new TestRailClient(testRailApi)); + + when(mockFuture.join()).thenReturn(mockResponse); + when(testRailApi.addPlan(anyLong(), any())).thenReturn(mockFuture); + when(testRailApi.addPlanEntry(anyLong(), any())).thenReturn(mockFuture); + when(testRailApi.addResult(anyLong(), any())).thenReturn(mockFuture); + when(testRailApi.getPlan(anyLong())).thenReturn(mockFuture); + when(testRailApi.getProject(anyLong())).thenReturn(mockFuture); + when(testRailApi.getStatuses()).thenReturn(mockFuture); + when(testRailApi.getSuite(anyLong())).thenReturn(mockFuture); + when(testRailApi.getTests(anyLong(), any(), anyInt(), anyInt())).thenReturn(mockFuture); + when(testRailApi.updatePlanEntry(anyLong(), anyString(), any())).thenReturn(mockFuture); + } + + public static void setResponseCode(Status status) { + when(mockResponse.code()).thenReturn(status.getStatusCode()); + when(mockResponse.isSuccessful()).thenReturn(status == Status.OK); + } + + public static void setResponseBody(Object body) { + when(mockResponse.body()).thenReturn(body); + } + + @SneakyThrows + @Test + public void testCreatePlanEntrySuccess() { + when(mockResponse.code()).thenReturn(Status.OK.getStatusCode()); + when(mockResponse.isSuccessful()).thenReturn(true); + setResponseCode(Status.OK); + final var result = PlanEntryDto.builder().build(); + setResponseBody(result); + assertEquals(client.createNewPlanEntry("run name", 0L, 0L, true, setOfZero), result); + } + + @Test + public void testCreatePlanEntryFailures() { + setResponseCode(Status.CONFLICT); + try { + client.createNewPlanEntry("run name", 0L, 0L, true, setOfZero); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.MAINTENANCE); + } + setResponseCode(Status.UNAUTHORIZED); + try { + client.createNewPlanEntry("run name", 0L, 0L, true, setOfZero); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.AUTHENTICATION_FAILED); + } + setResponseCode(Status.TOO_MANY_REQUESTS); + try { + client.createNewPlanEntry("run name", 0L, 0L, true, setOfZero); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.HIT_RATE_LIMIT); + } + setResponseCode(Status.BAD_REQUEST); + try { + client.createNewPlanEntry("run name", 0L, 0L, true, setOfZero); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.BAD_REQUEST); + } + setResponseCode(Status.FORBIDDEN); + try { + client.createNewPlanEntry("run name", 0L, 0L, true, setOfZero); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.ACCESS_DENIED); + } + } + + @SneakyThrows + @Test + public void testUpdateExistingPlanEntrySuccess() { + setResponseCode(Status.OK); + final var result = PlanEntryDto.builder().build(); + setResponseBody(result); + assertEquals(client.updateExistingPlanEntry(0L, "plan entry name", setOfZero), result); + } + + @SneakyThrows + @Test + public void testUpdateExistingPlanEntryFailures() { + setResponseCode(Status.CONFLICT); + try { + client.updateExistingPlanEntry(0L, "plan entry name", setOfZero); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.MAINTENANCE); + } + setResponseCode(Status.UNAUTHORIZED); + try { + client.updateExistingPlanEntry(0L, "plan entry name", setOfZero); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.AUTHENTICATION_FAILED); + } + setResponseCode(Status.TOO_MANY_REQUESTS); + try { + client.updateExistingPlanEntry(0L, "plan entry name", setOfZero); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.HIT_RATE_LIMIT); + } + setResponseCode(Status.BAD_REQUEST); + try { + client.updateExistingPlanEntry(0L, "plan entry name", setOfZero); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.BAD_REQUEST); + } + setResponseCode(Status.FORBIDDEN); + try { + client.updateExistingPlanEntry(0L, "plan entry name", setOfZero); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.ACCESS_DENIED); + } + } + + @SneakyThrows + @Test + public void testCreateTestPlanSuccess() { + setResponseCode(Status.OK); + final var result = PlanDto.builder().build(); + setResponseBody(result); + assertEquals(client.createTestPlan("Test Plan", 0L), result); + } + + @SneakyThrows + @Test + public void testCreateTestPlanFailures() { + setResponseCode(Status.CONFLICT); + try { + client.createTestPlan("Test Plan", 0L); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.MAINTENANCE); + } + setResponseCode(Status.UNAUTHORIZED); + try { + client.createTestPlan("Test Plan", 0L); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.AUTHENTICATION_FAILED); + } + setResponseCode(Status.TOO_MANY_REQUESTS); + try { + client.createTestPlan("Test Plan", 0L); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.HIT_RATE_LIMIT); + } + setResponseCode(Status.BAD_REQUEST); + try { + client.createTestPlan("Test Plan", 0L); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.BAD_REQUEST); + } + setResponseCode(Status.FORBIDDEN); + try { + client.createTestPlan("Test Plan", 0L); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.ACCESS_DENIED); + } + } + + @SneakyThrows + @Test + public void testGetTestPlanSuccess() { + setResponseCode(Status.OK); + final var result = PlanDto.builder().build(); + setResponseBody(result); + assertEquals(client.getTestPlan(0L), result); + } + + @SneakyThrows + @Test + public void testGetTestPlanFailures() { + setResponseCode(Status.CONFLICT); + try { + client.getTestPlan(0L); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.MAINTENANCE); + } + setResponseCode(Status.UNAUTHORIZED); + try { + client.getTestPlan(0L); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.AUTHENTICATION_FAILED); + } + setResponseCode(Status.TOO_MANY_REQUESTS); + try { + client.getTestPlan(0L); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.HIT_RATE_LIMIT); + } + setResponseCode(Status.BAD_REQUEST); + try { + client.getTestPlan(0L); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.BAD_REQUEST); + } + setResponseCode(Status.FORBIDDEN); + try { + client.getTestPlan(0L); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.ACCESS_DENIED); + } + } + + @SneakyThrows + @Test + public void testGetProjectSuccess() { + setResponseCode(Status.OK); + final var result = ProjectDto.builder().build(); + setResponseBody(result); + assertEquals(client.getProject(0L), result); + } + + @SneakyThrows + @Test + public void testGetProjectFailures() { + setResponseCode(Status.CONFLICT); + try { + client.getProject(0L); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.MAINTENANCE); + } + setResponseCode(Status.UNAUTHORIZED); + try { + client.getProject(0L); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.AUTHENTICATION_FAILED); + } + setResponseCode(Status.TOO_MANY_REQUESTS); + try { + client.getProject(0L); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.HIT_RATE_LIMIT); + } + setResponseCode(Status.BAD_REQUEST); + try { + client.getProject(0L); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.BAD_REQUEST); + } + setResponseCode(Status.FORBIDDEN); + try { + client.getProject(0L); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.ACCESS_DENIED); + } + } + + @SneakyThrows + @Test + public void testGetTestSuiteSuccess() { + setResponseCode(Status.OK); + final var result = TestSuiteDto.builder().build(); + setResponseBody(result); + assertEquals(client.getTestSuite(0L), result); + } + + @SneakyThrows + @Test + public void testGetTestSuiteFailures() { + setResponseCode(Status.CONFLICT); + try { + client.getTestSuite(0L); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.MAINTENANCE); + } + setResponseCode(Status.UNAUTHORIZED); + try { + client.getTestSuite(0L); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.AUTHENTICATION_FAILED); + } + setResponseCode(Status.TOO_MANY_REQUESTS); + try { + client.getTestSuite(0L); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.HIT_RATE_LIMIT); + } + setResponseCode(Status.BAD_REQUEST); + try { + client.getTestSuite(0L); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.BAD_REQUEST); + } + setResponseCode(Status.FORBIDDEN); + try { + client.getTestSuite(0L); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.ACCESS_DENIED); + } + } + + @SneakyThrows + @Test + public void testGetTestResultsForRunSuccess() { + setResponseCode(Status.OK); + final var result = + new PaginatedBulkTestDto( + 0, 0, 0, null, Collections.singletonList(TestDto.builder().build())); + setResponseBody(result); + assertEquals(client.getTestResultsForRun(0L, null), result.tests()); + } + + @SneakyThrows + @Test + public void testGetTestResultsForRunFailures() { + setResponseCode(Status.CONFLICT); + try { + client.getTestResultsForRun(0L, null); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.MAINTENANCE); + } + setResponseCode(Status.UNAUTHORIZED); + try { + client.getTestResultsForRun(0L, null); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.AUTHENTICATION_FAILED); + } + setResponseCode(Status.TOO_MANY_REQUESTS); + try { + client.getTestResultsForRun(0L, null); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.HIT_RATE_LIMIT); + } + setResponseCode(Status.BAD_REQUEST); + try { + client.getTestResultsForRun(0L, null); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.BAD_REQUEST); + } + setResponseCode(Status.FORBIDDEN); + try { + client.getTestResultsForRun(0L, null); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.ACCESS_DENIED); + } + } + + @SneakyThrows + @Test + public void testRetrieveStatusesFromTestRailsSuccess() { + setResponseCode(Status.OK); + final var result = + Arrays.asList( + StatusDto.builder().id(0).label("label 0").name("name 0").build(), + StatusDto.builder().id(1).label("label 1").name("name 1").build(), + StatusDto.builder().id(2).label("label 2").name("name 2").build(), + StatusDto.builder().id(3).label("label 3").name("name 3").build()); + setResponseBody(result); + assertEquals(client.getCustomStatuses(), result); + } + + @SneakyThrows + @Test + public void testRetrieveStatusesFromTestRailFailures() { + setResponseCode(Status.CONFLICT); + try { + client.getCustomStatuses(); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.MAINTENANCE); + } + setResponseCode(Status.UNAUTHORIZED); + try { + client.getCustomStatuses(); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.AUTHENTICATION_FAILED); + } + setResponseCode(Status.TOO_MANY_REQUESTS); + try { + client.getCustomStatuses(); + } catch (TestRailException e) { + assertEquals(e.getStatus(), TestRailErrorStatus.HIT_RATE_LIMIT); + } + } +} diff --git a/src/test/java/com/applause/auto/testrail/client/TestRailParamValidatorTest.java b/src/test/java/com/applause/auto/testrail/client/TestRailParamValidatorTest.java new file mode 100644 index 0000000..2c8e9d9 --- /dev/null +++ b/src/test/java/com/applause/auto/testrail/client/TestRailParamValidatorTest.java @@ -0,0 +1,185 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.applause.auto.testrail.client.errors.TestRailException; +import com.applause.auto.testrail.client.models.config.TestRailStatusMaps; +import com.applause.auto.testrail.client.models.testrail.PaginatedBulkCaseDto; +import com.applause.auto.testrail.client.models.testrail.ProjectDto; +import com.applause.auto.testrail.client.models.testrail.StatusDto; +import com.applause.auto.testrail.client.models.testrail.TestSuiteDto; +import java.util.Collections; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import lombok.NonNull; +import okhttp3.OkHttpClient; +import org.apache.commons.lang3.function.FailableRunnable; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import retrofit2.Response; + +public class TestRailParamValidatorTest { + + @BeforeAll + public static void beforeAll() { + OkHttpClient httpClient = mock(OkHttpClient.class); + // short-circuit client rebuilding to return original mock lol + var clientBuilder = mock(OkHttpClient.Builder.class); + when(clientBuilder.addInterceptor(any())).thenReturn(clientBuilder); + when(clientBuilder.build()).thenReturn(httpClient); + when(httpClient.newBuilder()).thenReturn(clientBuilder); + } + + @Test + public void testValidationLogicNullTestrailParams() throws TestRailException { + + final TestRailApi apiMock = getTestRailApiMocked(true, true); + var paramValidator = makeParamValidator(apiMock); + + TestRailStatusMaps statuses = new TestRailStatusMaps(1, 2, 3, 4, 5); + // everything okay + paramValidator.validateTestrailConfiguration(1L, 10L, "planName", "runName", statuses); + // null project id + assertThrowsWithMessage( + "Invalid testRailProjectId", + () -> + paramValidator.validateTestrailConfiguration( + null, 10L, "planName", "runName", statuses)); + // negative project id + assertThrowsWithMessage( + "Invalid testRailProjectId", + () -> + paramValidator.validateTestrailConfiguration( + -1L, 10L, "planName", "runName", statuses)); + // null suite id + assertThrowsWithMessage( + "Invalid testRailSuiteId", + () -> + paramValidator.validateTestrailConfiguration( + 1L, null, "planName", "runName", statuses)); + // negative suite id + assertThrowsWithMessage( + "Invalid testRailSuiteId", + () -> + paramValidator.validateTestrailConfiguration( + 1L, -10L, "planName", "runName", statuses)); + // null plan name + assertThrowsWithMessage( + "Invalid testRailPlanName", + () -> paramValidator.validateTestrailConfiguration(1L, 10L, null, "runName", statuses)); + // null run name + assertThrowsWithMessage( + "Invalid testRailRunName", + () -> paramValidator.validateTestrailConfiguration(1L, 10L, "planName", null, statuses)); + } + + @Test + void checkTestrailErrorResponses() { + TestRailStatusMaps statuses = new TestRailStatusMaps(1, 2, 3, 4, 5); + assertThrowsWithMessage( + "An unknown TestRail error has occurred. Details: Could not get TestRail project: 1", + () -> + makeParamValidator(getTestRailApiMocked(false, true)) + .validateTestrailConfiguration(1L, 10L, "planName", "runName", statuses)); + + assertThrowsWithMessage( + "An unknown TestRail error has occurred. Details: Could not get TestRail test suite: 10", + () -> + makeParamValidator(getTestRailApiMocked(true, false)) + .validateTestrailConfiguration(1L, 10L, "planName", "runName", statuses)); + } + + private TestRailParamValidator makeParamValidator(@NonNull final TestRailApi apiMock) { + return new TestRailParamValidator(new TestRailClient(apiMock)); + } + + @NonNull + private TestRailApi getTestRailApiMocked( + final boolean projectResponseSuccess, final boolean suiteResponseSuccess) { + var apiMock = mock(TestRailApi.class); + Response projectResponseMock = Mockito.mock(); + Response suiteResponseMock = Mockito.mock(); + Response statusResponseMock = Mockito.mock(); + Response casesResponseMock = Mockito.mock(); + when(projectResponseMock.code()).thenReturn(200); + when(suiteResponseMock.code()).thenReturn(200); + when(statusResponseMock.code()).thenReturn(200); + when(casesResponseMock.code()).thenReturn(200); + + doReturn(projectResponseSuccess).when(projectResponseMock).isSuccessful(); + doReturn(suiteResponseSuccess).when(suiteResponseMock).isSuccessful(); + doReturn(true).when(statusResponseMock).isSuccessful(); + doReturn(true).when(casesResponseMock).isSuccessful(); + + doReturn(new TestSuiteDto(1, "hi", 0, false, false, false, "name", 1, "url")) + .when(suiteResponseMock) + .body(); + doReturn(mock(ProjectDto.class)).when(projectResponseMock).body(); + doReturn(Collections.emptyList()).when(statusResponseMock).body(); + doReturn(new PaginatedBulkCaseDto(0, 0, 0, null, Collections.emptyList())) + .when(casesResponseMock) + .body(); + + when(apiMock.getProject(anyLong())) + .thenReturn(CompletableFuture.completedFuture(projectResponseMock)); + when(apiMock.getSuite(anyLong())) + .thenReturn(CompletableFuture.completedFuture(suiteResponseMock)); + doReturn(CompletableFuture.completedFuture(statusResponseMock)).when(apiMock).getStatuses(); + when(apiMock.getCasesForSuite(anyLong(), anyLong(), anyInt(), anyInt())) + .thenReturn(CompletableFuture.completedFuture(casesResponseMock)); + return apiMock; + } + + private static final class LambdaDidntThrowException extends RuntimeException {} + + private void assertThrowsWithMessage( + @NonNull final String msgSubStr, @NonNull FailableRunnable func) { + try { + func.run(); + // throw if running lambda didn't throw an exception lol + throw new LambdaDidntThrowException(); + } catch (LambdaDidntThrowException e) { + throw new RuntimeException("Lambda didn't throw an exception as expected"); + } catch (Exception ex) { + // rethrow if not exception type we want + if (!(ex instanceof TestRailException)) { + throw new RuntimeException( + "Exception class didn't match expected " + + TestRailException.class.getName() + + " , was: " + + ex.getClass().getName(), + ex); + } + // throw if exception message doesn't match + if (!Optional.ofNullable(ex.getMessage()) + .map(exMsg -> exMsg.contains(msgSubStr)) + .orElse(false)) { + throw new RuntimeException( + "Exception message '" + ex.getMessage() + "' did not contain '" + msgSubStr + "'"); + } + } + } +} diff --git a/src/test/java/com/applause/auto/testrail/client/TestRailUtilTest.java b/src/test/java/com/applause/auto/testrail/client/TestRailUtilTest.java new file mode 100644 index 0000000..6a5df23 --- /dev/null +++ b/src/test/java/com/applause/auto/testrail/client/TestRailUtilTest.java @@ -0,0 +1,102 @@ +/* +* +* Copyright © 2023 Applause App Quality, Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ +package com.applause.auto.testrail.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.applause.auto.testrail.client.enums.TestResultStatus; +import com.applause.auto.testrail.client.models.config.TestRailDto; +import org.junit.jupiter.api.Test; + +public class TestRailUtilTest { + + @Test + public void testValidateTestRailCaseIdNull() { + assertFalse(TestRailUtil.validateTestRailCaseId(null), "should never allow a null value"); + } + + @Test + public void testValidateTestRailCaseIdBlank() { + assertFalse(TestRailUtil.validateTestRailCaseId(""), "should never allow a null value"); + } + + @Test + public void testValidateTestRailCaseIdPositive() { + assertTrue(TestRailUtil.validateTestRailCaseId("1"), "small number should work"); + assertTrue( + TestRailUtil.validateTestRailCaseId("1234567890987654321"), "large number should work"); + assertTrue(TestRailUtil.validateTestRailCaseId("C1"), "small number with C prefix should work"); + assertTrue( + TestRailUtil.validateTestRailCaseId("C1234567890987654321"), + "big number with C prefix should work"); + } + + @Test + public void testValidateTestRailCaseIdNegative() { + assertFalse( + TestRailUtil.validateTestRailCaseId("abc"), "non-number characters should not work"); + assertFalse(TestRailUtil.validateTestRailCaseId("123.456"), "floats should not work"); + assertFalse(TestRailUtil.validateTestRailCaseId("-17643"), "negative numbers should not work"); + + assertFalse(TestRailUtil.validateTestRailCaseId("`"), "non-number character should not work"); + assertFalse(TestRailUtil.validateTestRailCaseId("FTX-680"), "JIRA callouts should not work"); + assertFalse(TestRailUtil.validateTestRailCaseId("[null]"), "fake null values should not work"); + } + + @Test + public void testBuildCustomStatusMap() { + final var statusPassed = 931; + final var statusFailed = 446; + final var statusSkipped = 1112; + final var statusError = 12421; + final var statusCanceled = 8465; + + final var testRailDto = + new TestRailDto() + .setMappedStatusPassed(statusPassed) + .setMappedStatusFailed(statusFailed) + .setMappedStatusSkipped(statusSkipped) + .setMappedStatusError(statusError) + .setMappedStatusCanceled(statusCanceled); + + final var returnedMap = TestRailUtil.buildCustomStatusMapping(testRailDto); + + assertEquals( + statusPassed, + returnedMap.get(TestResultStatus.PASSED).intValue(), + "should have right property"); + assertEquals( + statusFailed, + returnedMap.get(TestResultStatus.FAILED).intValue(), + "should have right property"); + assertEquals( + statusSkipped, + returnedMap.get(TestResultStatus.SKIPPED).intValue(), + "should have right property"); + assertEquals( + statusError, + returnedMap.get(TestResultStatus.ERROR).intValue(), + "should have right property"); + assertEquals( + statusCanceled, + returnedMap.get(TestResultStatus.CANCELED).intValue(), + "should have right property"); + } +} diff --git a/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000..1f0955d --- /dev/null +++ b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline