From 3c6164c393d1081a164500ff66bd6cbb0731840b Mon Sep 17 00:00:00 2001 From: Sylvain NIEUWLANDT <45944760+dktsni@users.noreply.github.com> Date: Tue, 9 Apr 2019 09:42:58 +0200 Subject: [PATCH] Release 3.1.0 Release 3.1.0 : * Create the Github DefectAdapter * Add legal and utility documents for OSS release (license, contributing...) * Use hardcoded url to Github instead of generated ones. --- CHANGELOG.adoc | 15 ++ LICENSE.adoc | 208 +++++++++++++++ README.adoc | 18 ++ client/pom.xml | 2 +- client/src/components/top-menu.vue | 4 +- .../src/views/functionality-cartography.vue | 2 +- code-of-conduct.adoc | 33 +++ contributing.adoc | 19 ++ contributor-licence-agreement.adoc | 40 +++ db/manage-db.sh | 0 doc/developer/DeveloperDocumentation.adoc | 1 - final/pom.xml | 2 +- generated-cucumber-report/pom.xml | 2 +- lib/pom.xml | 2 +- pom.xml | 2 +- server/pom.xml | 8 +- .../defect/github/GithubDefectAdapter.java | 124 +++++++++ .../ara/defect/github/GithubIssue.java | 21 ++ .../ara/defect/github/GithubMapper.java | 64 +++++ .../ara/defect/github/GithubRestClient.java | 185 ++++++++++++++ .../ara/service/SettingProviderService.java | 51 +++- .../ara/service/support/Settings.java | 6 +- .../main/resources/application-dev.properties | 2 +- .../ara/defect/github/GithubMapperTest.java | 89 +++++++ .../defect/github/GithubRestClientTest.java | 237 ++++++++++++++++++ 25 files changed, 1116 insertions(+), 21 deletions(-) create mode 100644 LICENSE.adoc create mode 100644 code-of-conduct.adoc create mode 100644 contributing.adoc create mode 100644 contributor-licence-agreement.adoc mode change 100644 => 100755 db/manage-db.sh create mode 100644 server/src/main/java/com/decathlon/ara/defect/github/GithubDefectAdapter.java create mode 100644 server/src/main/java/com/decathlon/ara/defect/github/GithubIssue.java create mode 100644 server/src/main/java/com/decathlon/ara/defect/github/GithubMapper.java create mode 100644 server/src/main/java/com/decathlon/ara/defect/github/GithubRestClient.java create mode 100644 server/src/test/java/com/decathlon/ara/defect/github/GithubMapperTest.java create mode 100644 server/src/test/java/com/decathlon/ara/defect/github/GithubRestClientTest.java diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 70ddb2302..506f93a7a 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -22,6 +22,18 @@ For instance, on Windows, you can use http://getgreenshot.org/ for screenshots and https://www.screentogif.com/ for animated-GIFs. //// +== 3.1.0 + +== User-Visible Changes + +* *[FEATURE]* Add support for GitHub issue tracker. + +== Technical Changes + +* *[FIX]* Fix an issue where a Swagger dependency can't be downloaded on first install. +* *[FEATURE]* Release ARA source code under Apache 2 license on Github : +https://github.com/decathlon/ara + == 3.0.1 === Technical Changes @@ -33,6 +45,9 @@ to freeze, when the execution have several errors with detailled exceptions. First version of ARA with a release of the source code. +* *[BREAKING CHANGE]* Change the way the final jar is builded +* *[FEATURE]* Connect the project to Decathlon's internal CI. + === Technical Changes * *[BREAKING CHANGE]* Change the way the final Executable is builded. diff --git a/LICENSE.adoc b/LICENSE.adoc new file mode 100644 index 000000000..1b9f26cbe --- /dev/null +++ b/LICENSE.adoc @@ -0,0 +1,208 @@ += ARA License (Apache 2 License) + +== Copyright (C) 2019 by Decathlon + +``` + 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.adoc b/README.adoc index 7c1c76343..f8aa84567 100644 --- a/README.adoc +++ b/README.adoc @@ -32,6 +32,24 @@ You only need a report.json from Cucumber and/or one or several JSON reports fro ARA integrates best with a working Continuous Integration pipeline which launches your tests automatically (on a time-schedule or after each commit). + You will then modify your pipeline slightly to send such report files to ARA. +== How to build it and make it work in 2 minutes + +After you've download the source code of ARA, here is all you have to do to make it run on your local environment. You will +need : + +* Java 8 (for now, supports for more recent Java versions will come soon). +* Maven 3 +* Docker +* A shell environment (either Bash, Sh, Zsh..., or even a git-bash on Windows) + +In the following, the folder which contains this Readme will be called `ARA_ROOT` + +. Create a folder where your database's datas are going to live (says `my/db/path`) +. Open a Terminal, and go to `ARA_ROOT/db` +. Use `./manage-db.sh create my/db/path`, this command will create a MySQL Dockerized database locally. +. In the terminal, go back to `ARA_ROOT`` and a `mvn clean install -Pdev -DskipPitest` +. Go to `ARA_ROOT/final/target` and run `java -Dspring.profiles.active=dev -jar ara-{{YOUR_VERSION}}.jar` + == How to Set Up ARA? Please read the <> diff --git a/client/pom.xml b/client/pom.xml index 5e9b04848..3e3ef1752 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -5,7 +5,7 @@ com.decathlon.ara ara-parent - 3.0.1 + 3.1.0-SNAPSHOT ara-client jar diff --git a/client/src/components/top-menu.vue b/client/src/components/top-menu.vue index 4ac274013..aa5744ea9 100644 --- a/client/src/components/top-menu.vue +++ b/client/src/components/top-menu.vue @@ -30,11 +30,11 @@
- - diff --git a/client/src/views/functionality-cartography.vue b/client/src/views/functionality-cartography.vue index cb89f486b..b3c5642cd 100644 --- a/client/src/views/functionality-cartography.vue +++ b/client/src/views/functionality-cartography.vue @@ -19,7 +19,7 @@
- How to create & move functionalities diff --git a/code-of-conduct.adoc b/code-of-conduct.adoc new file mode 100644 index 000000000..c3c96c666 --- /dev/null +++ b/code-of-conduct.adoc @@ -0,0 +1,33 @@ += Decathlon IT Code of Conduct + +=== Adapted from https://www.djangoproject.com/conduct/ © Django Project. + +Like the technical community as a whole, the Decathlon IT and community is made up of a mixture of professionals and volunteers from all over the world, working on every aspect of the mission - including mentorship, and connecting people. + +Diversity is one of our huge strengths, but it can also lead to communication issues and unhappiness. To that end, we have a few ground rules that we ask people to adhere to. This code applies equally to founders, mentors and those seeking help and guidance. + +This isn’t an exhaustive list of things that you can’t do. Rather, take it in the spirit in which it’s intended - a guide to make it easier to enrich all of us and the technical communities in which we participate. + +This code of conduct applies to all spaces managed by the Decathlon IT. This includes IRC, the mailing lists, the issue tracker, events, and any other forums created by the project team which the community uses for communication. In addition, violations of this code outside these spaces may affect a person's ability to participate within them. + +If you believe someone is violating the code of conduct, we ask that you report it by emailing oss@decathlon.com. + +- **Be friendly and patient**. +- **Be welcoming**. We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. + +- **Be considerate**. Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that we're a world-wide community, so you might not be communicating in someone else's primary language. + +- **Be respectful**. Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It’s important to remember that a community where people feel uncomfortable or threatened is not a productive one. Members of the Decathlon community should be respectful when dealing with other members as well as with people outside the Decathlon community. + +- **Be careful in the words that you choose**. We are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren't acceptable. This includes, but is not limited to: + + - [ ] Violent threats or language directed against another person. + - [ ] Discriminatory jokes and language. + - [ ] Posting sexually explicit or violent material. + - [ ] Posting (or threatening to post) other people's personally identifying information ("doxing"). + - [ ] Personal insults, especially those using racist or sexist terms. + - [ ] Unwelcome sexual attention. + - [ ] Advocating for, or encouraging, any of the above behavior. + - [ ] Repeated harassment of others. In general, if someone asks you to stop, then stop. + +- When we disagree, try to understand why. Disagreements, both social and technical, happen all the time and Decathlon IT is no exception. It is important that we resolve disagreements and differing views constructively. Remember that we’re different. The strength of Decathlon comes from its varied community, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is human to error and blaming each other doesn’t get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes. \ No newline at end of file diff --git a/contributing.adoc b/contributing.adoc new file mode 100644 index 000000000..e85dc9326 --- /dev/null +++ b/contributing.adoc @@ -0,0 +1,19 @@ += Contributing + +Before contributing, please read carefully, complete and sign our [Contributor Licence Agreement](contributor-licence-agreement.adoc). + +When contributing to this repository, please first discuss the change you wish to make via issue or any other available method with the owners of this repository before making a change. + +Please note we have a [code of conduct](code-of-conduct.adoc), please follow it in all your interactions with the project. + +== Pull Request Process + +. Ensure any install or build dependencies are removed before the end of the layer when doing a + build. +. Update the README.adoc with details of changes to the interface, this includes new environment + variables, exposed ports, useful file locations and container parameters. +. Increase the version numbers in any examples files and the README.adoc to the new version that this + Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). +. Squash all your commits before creating the Pull Request. (One commit per feature/issue) +. You may merge the Pull Request in once you have the sign-off of two other developers, or if you + do not have permission to do that, you may request the second reviewer to merge it for you. \ No newline at end of file diff --git a/contributor-licence-agreement.adoc b/contributor-licence-agreement.adoc new file mode 100644 index 000000000..16ee95db7 --- /dev/null +++ b/contributor-licence-agreement.adoc @@ -0,0 +1,40 @@ += Decathlon Individual Contributor License Agreement + +=== *Adapted from http://www.apache.org/licenses/ © Apache Software Foundation.* + +In order to clarify the intellectual property license granted with Contributions from any person or entity, Decathlon must have a Contributor License Agreement ("CLA") on file that has been signed by each Contributor, indicating agreement to the license terms below. This CLA is for your protection as a Contributor as well as the protection of Decathlon and its users; it does not change your rights to use your own Contributions for any other purpose. + +If you have not already done so, please complete and sign, then scan and email a pdf file of this CLA to [oss@decathlon.com](mailto:oss@decathlon.com). + +Please read this document carefully before signing and keep a copy for your records. + +You accept and agree to the following terms and conditions for your present and future Contributions Submitted to Decathlon. In return, Decathlon shall not use your Contributions in a way that is contrary to the public benefit or inconsistent with its nonprofit status and bylaws in effect at the time of the Contribution. Except for the license granted herein to Decathlon and recipients of software distributed by Decathlon, You reserve all right, title, and interest in and to your Contributions. + +1. Definitions. "You" (or "Contributor") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this CLA with Decathlon. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. + + 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. + + "Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally Submitted by You to Decathlon for inclusion in, or documentation of, any of the products owned or managed by Decathlon (the "Work"). For the purposes of this definition, "Submitted" means any form of electronic, verbal, or written communication sent to Decathlon 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, Decathlon for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." + +2. Grant of Copyright License. Subject to the terms and conditions of this CLA, You hereby grant to Decathlon and to recipients of software distributed by Decathlon 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 your Contributions and such derivative works. + +3. Grant of Patent License. Subject to the terms and conditions of this CLA, You hereby grant to Decathlon and to recipients of software distributed by Decathlon 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 You that are necessarily infringed by your Contribution(s) alone or by combination of your Contribution(s) with the Work to which such Contribution(s) was Submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this CLA for that Contribution or Work shall terminate as of the date such litigation is filed. + +4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to Decathlon, or that your employer has executed a separate Corporate CLA with Decathlon. + +5. You represent that each of your Contributions is your original creation (see section 7 for submissions on behalf of others). You represent that your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of your Contributions. + +6. You are not expected to provide support for your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide your 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. + +7. You commit not to copy code from another project which license does not allow the duplication / reuse / modification of their source code and / or license is not compatible with the project you are contributing to. As a reminder, a project without an explicit license must be considered as a project with a copyrighted license. + +8. You agree to notify Decathlon of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect. + +|=== +| Full name | [underline]#                                # +| Postal Address | [underline]#                                # +| | [underline]#                                # +| Email | [underline]#                                # +| Github handle | [underline]#                                # +| Date | [underline]#                                # +| Sign | [underline]#                                # \ No newline at end of file diff --git a/db/manage-db.sh b/db/manage-db.sh old mode 100644 new mode 100755 diff --git a/doc/developer/DeveloperDocumentation.adoc b/doc/developer/DeveloperDocumentation.adoc index bb60fcc08..6c2b77463 100644 --- a/doc/developer/DeveloperDocumentation.adoc +++ b/doc/developer/DeveloperDocumentation.adoc @@ -10,7 +10,6 @@ Here is basically what you will need to set up on your development machine: ** Run `mvn install -Pdev_in` in the project's root folder to build the server and its dependencies (you can speed up the build by running `mvn install -Pdev_in -DskipTests -DskipITs -DskipPitest` instead). ** Run `java -Dspring.profiles.active=dev -jar server/target/ara-server-*.jar` to launch the back-end server. + - ** APIs are available here: http://localhost:8080/swagger-ui.html + ** When modifying the server, just run `mvn install -Pdev_in` in the `server` folder to not rebuild all dependencies, this time. * *Build the client part and start it:* ** Run `npm install` (this is part of the above `mvn install`, so you can skip it if you have already done that). diff --git a/final/pom.xml b/final/pom.xml index a159988a1..88be5ad24 100644 --- a/final/pom.xml +++ b/final/pom.xml @@ -6,7 +6,7 @@ com.decathlon.ara ara-parent - 3.0.1 + 3.1.0-SNAPSHOT ara-final jar diff --git a/generated-cucumber-report/pom.xml b/generated-cucumber-report/pom.xml index 0b0f1274e..13cd5209b 100644 --- a/generated-cucumber-report/pom.xml +++ b/generated-cucumber-report/pom.xml @@ -5,7 +5,7 @@ com.decathlon.ara ara-parent - 3.0.1 + 3.1.0-SNAPSHOT ara-generated-cucumber-report diff --git a/lib/pom.xml b/lib/pom.xml index b0caf67d6..05f4c7086 100644 --- a/lib/pom.xml +++ b/lib/pom.xml @@ -5,7 +5,7 @@ com.decathlon.ara ara-parent - 3.0.1 + 3.1.0-SNAPSHOT ara-lib diff --git a/pom.xml b/pom.xml index 0f9d4c777..7535c8687 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ com.decathlon.ara ara-parent pom - 3.0.1 + 3.1.0-SNAPSHOT Agile Regression Analyzer - Parent diff --git a/server/pom.xml b/server/pom.xml index bbb725952..7227331da 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -6,7 +6,7 @@ com.decathlon.ara ara-parent - 3.0.1 + 3.1.0-SNAPSHOT ara-server jar @@ -669,6 +669,9 @@ jar + + false + --> diff --git a/server/src/main/java/com/decathlon/ara/defect/github/GithubDefectAdapter.java b/server/src/main/java/com/decathlon/ara/defect/github/GithubDefectAdapter.java new file mode 100644 index 000000000..f5febf442 --- /dev/null +++ b/server/src/main/java/com/decathlon/ara/defect/github/GithubDefectAdapter.java @@ -0,0 +1,124 @@ +package com.decathlon.ara.defect.github; + +import com.decathlon.ara.ci.util.FetchException; +import com.decathlon.ara.defect.DefectAdapter; +import com.decathlon.ara.defect.bean.Defect; +import com.decathlon.ara.domain.enumeration.ProblemStatus; +import com.decathlon.ara.service.SettingProviderService; +import com.decathlon.ara.service.SettingService; +import com.decathlon.ara.service.dto.setting.SettingDTO; +import com.decathlon.ara.service.support.Settings; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + + +/** + * Implementation of {@link DefectAdapter} for Github issues system. + * + * @author Sylvain Nieuwlandt + * @since 3.1.0 + */ +@Slf4j +@Service +@RequiredArgsConstructor(onConstructor = @__(@Autowired)) +public class GithubDefectAdapter implements DefectAdapter { + private static final String UNABLE_TO_ACCESS_GITHUB = "Unable to access Github."; + @Autowired + private GithubRestClient restClient; + + @Autowired + private SettingService settingService; + + @Autowired + private SettingProviderService settingProviderService; + + + @Override + public List getStatuses(long projectId, List ids) throws FetchException { + String repositoryOwner = this.settingService.get(projectId, Settings.DEFECT_GITHUB_OWNER); + String repositoryName = this.settingService.get(projectId, Settings.DEFECT_GITHUB_REPONAME); + String authorizationToken = this.settingService.get(projectId, Settings.DEFECT_GITHUB_TOKEN); + List issueIds = ids.stream() + .map(Long::valueOf) + .collect(Collectors.toList()); + + try { + return this.restClient + .forOwnerAndRepository(repositoryOwner, repositoryName) + .withToken(authorizationToken) + .requestIssues(issueIds) + .stream() + .map(this::toDefect) + .collect(Collectors.toList()); + } catch (IOException | URISyntaxException ex) { + log.error(UNABLE_TO_ACCESS_GITHUB, ex); + throw new FetchException(UNABLE_TO_ACCESS_GITHUB, ex); + } + } + + @Override + public List getChangedDefects(long projectId, Date since) throws FetchException { + String repositoryOwner = this.settingService.get(projectId, Settings.DEFECT_GITHUB_OWNER); + String repositoryName = this.settingService.get(projectId, Settings.DEFECT_GITHUB_REPONAME); + String authorizationToken = this.settingService.get(projectId, Settings.DEFECT_GITHUB_TOKEN); + try { + return this.restClient + .forOwnerAndRepository(repositoryOwner, repositoryName) + .withToken(authorizationToken) + .getIssuesUpdatedSince(since) + .stream() + .map(this::toDefect) + .collect(Collectors.toList()); + } catch (IOException | URISyntaxException ex) { + log.error(UNABLE_TO_ACCESS_GITHUB, ex); + throw new FetchException(UNABLE_TO_ACCESS_GITHUB, ex); + } + } + + @Override + public boolean isValidId(long projectId, String id) { + try { + Integer value = Integer.valueOf(id); + return value > 0; + } catch (NumberFormatException ex) { + return false; + } + } + + @Override + public String getIdFormatHint(long projectId) { + return "The ID must be a positive integer."; + } + + @Override + public String getCode() { + return "github"; + } + + @Override + public String getName() { + return "GitHub"; + } + + @Override + public List getSettingDefinitions() { + return this.settingProviderService.getDefectGithubDefinitions(); + } + + private Defect toDefect(GithubIssue issue) { + String id = String.valueOf(issue.getNumber()); + ProblemStatus status = ProblemStatus.CLOSED; + if ("open".equals(issue.getState())) { + status = ProblemStatus.OPEN; + } + return new Defect(id, status, issue.getClosedAt()); + } +} diff --git a/server/src/main/java/com/decathlon/ara/defect/github/GithubIssue.java b/server/src/main/java/com/decathlon/ara/defect/github/GithubIssue.java new file mode 100644 index 000000000..e32a035cb --- /dev/null +++ b/server/src/main/java/com/decathlon/ara/defect/github/GithubIssue.java @@ -0,0 +1,21 @@ +package com.decathlon.ara.defect.github; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +@Data +@NoArgsConstructor +@AllArgsConstructor +class GithubIssue { + + private String url; + private long number; + private String title; + private String state; + private Date createdAt; + private Date updatedAt; + private Date closedAt; +} diff --git a/server/src/main/java/com/decathlon/ara/defect/github/GithubMapper.java b/server/src/main/java/com/decathlon/ara/defect/github/GithubMapper.java new file mode 100644 index 000000000..ce8f331e0 --- /dev/null +++ b/server/src/main/java/com/decathlon/ara/defect/github/GithubMapper.java @@ -0,0 +1,64 @@ +package com.decathlon.ara.defect.github; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Map the Github API Json responses to Java POJO in this project. + * + * @author Sylvain Nieuwlandt + * @since 3.1.0 + */ +@Service +@Slf4j +@RequiredArgsConstructor(onConstructor = @__(@Autowired)) +class GithubMapper { + static final TypeReference TYPE_REFERENCE_TO_GITHUB_ISSUE = + new TypeReference() { + }; + static final TypeReference> TYPE_REFERENCE_TO_LIST_GITHUB_ISSUE = + new TypeReference>() { + }; + + @Autowired + private final ObjectMapper objectMapper; + + /** + * Map the given json String to a GithubIssue. + * + * @param json the GitHub REST API response json + * @return an optional containing the GithubIssue or an empty one if the json is malformed / don't match. + */ + Optional jsonToIssue(String json) { + try { + return Optional.of(this.objectMapper.readValue(json, TYPE_REFERENCE_TO_GITHUB_ISSUE)); + } catch (IOException ex) { + log.warn("Unable to cast this json to a github issue : " + json, ex); + return Optional.empty(); + } + } + + /** + * Map the given json array String to a list GithubIssue. + * + * @param json the GitHub REST API response json + * @return the list of GithubIssue or an empty list if the json is malformed / don't match. + */ + List jsonToIssueList(String json) { + try { + return this.objectMapper.readValue(json, TYPE_REFERENCE_TO_LIST_GITHUB_ISSUE); + } catch (IOException ex) { + log.warn("Unable to cast this json to a list of github issues : " + json, ex); + return new ArrayList<>(); + } + } +} diff --git a/server/src/main/java/com/decathlon/ara/defect/github/GithubRestClient.java b/server/src/main/java/com/decathlon/ara/defect/github/GithubRestClient.java new file mode 100644 index 000000000..eac63d6e6 --- /dev/null +++ b/server/src/main/java/com/decathlon/ara/defect/github/GithubRestClient.java @@ -0,0 +1,185 @@ +package com.decathlon.ara.defect.github; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.impl.client.HttpClients; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Optional; + +/** + * Provide Java implementation of the GitHub REST API. + * + * @author Sylvain Nieuwlandt + * @since 3.1.0 + */ +@Slf4j +@Service +@RequiredArgsConstructor(onConstructor = @__(@Autowired)) +class GithubRestClient { + static final String PROTOCOL = "https"; + static final String BASEPATH = "api.github.com"; + + @Autowired + private GithubMapper githubMapper; + + private HttpClient httpClient; + private String currentOwner; + private String currentRepo; + private String currentAuthToken; + + /** + * Define the owner and repository to use for the next requests. + * + * @param owner the owner (user or organization) of the Repository + * @param repo the name of the Repository + * @return the calling instance + */ + GithubRestClient forOwnerAndRepository(String owner, String repo) { + if (this.isNotEmpty(owner)) { + this.currentOwner = owner; + } + if (this.isNotEmpty(repo)) { + this.currentRepo = repo; + } + return this; + } + + /** + * Sets the Authorization token to use for the next requests (usually a Personal Access Token). + * + * @param token the Authorization token + * @return the calling instance + */ + GithubRestClient withToken(String token) { + if (this.isNotEmpty(token)) { + this.currentAuthToken = token; + } + return this; + } + + /** + * Request the informations about the given issue, based on the owner and repository given before + * ({@link GithubRestClient#forOwnerAndRepository(String, String)}) + * + * @param issueId the id of the wanted issue + * @return the informations about the issue in a POJO. + * @throws IOException if Github can't be accessed + * @throws URISyntaxException if the informations provided into the owner and repository name are invalid in the URI. + */ + Optional requestIssue(long issueId) throws IOException, URISyntaxException { + this.prepareClient(); + String repoPath = this.currentOwner + "/" + this.currentRepo; + URI uri = new URIBuilder() + .setScheme(PROTOCOL) + .setHost(BASEPATH) + .setPath("/repos/" + repoPath + "/issues/" + issueId) + .setParameter("filter", "all") + .setParameter("state", "all") + .build(); + HttpGet request = new HttpGet(uri); + request.addHeader("Authorization", "token " + this.currentAuthToken); + HttpResponse response = this.httpClient.execute(request); + int responseCode = response.getStatusLine().getStatusCode(); + if (404 == responseCode || 410 == responseCode) { + return Optional.empty(); + } else if (200 == responseCode) { + return this.githubMapper.jsonToIssue(this.getContentOf(response)); + } else { + String msg = "Error while requesting issue " + issueId + " on repo " + repoPath + " : " + responseCode; + log.warn(msg); + throw new IOException(msg); + } + } + + /** + * Request the informations about several issues, based on the owner and repository given before + * ({@link GithubRestClient#forOwnerAndRepository(String, String)}) + * + * @param issueIds the list of id of the wanted issues + * @return the informations about the issues in a POJO. The list will contains only the issue with existing ids. + * @throws IOException if Github can't be accessed + * @throws URISyntaxException if the informations provided into the owner and repository name are invalid in the URI. + */ + List requestIssues(List issueIds) throws IOException, URISyntaxException { + List result = new ArrayList<>(); + for (Long issueId : issueIds) { + this.requestIssue(issueId).ifPresent(result::add); + } + return result; + } + + /** + * Request the informations about all the issues which has been updated since the given date, based on the owner + * and repository given before ({@link GithubRestClient#forOwnerAndRepository(String, String)}) + * + * @param time the start timestamp to search issues. + * @return the informations about the issues in a POJO. + * @throws IOException if Github can't be accessed + * @throws URISyntaxException if the informations provided into the owner and repository name are invalid in the URI. + */ + List getIssuesUpdatedSince(Date time) throws IOException, URISyntaxException { + this.prepareClient(); + List result = new ArrayList<>(); + String repoPath = this.currentOwner + "/" + this.currentRepo; + String date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").format(time); + URI uri = new URIBuilder() + .setScheme(PROTOCOL) + .setHost(BASEPATH) + .setPath("/repos/" + repoPath + "/issues") + .setParameter("filter", "all") + .setParameter("state", "all") + .setParameter("since", date) + .build(); + HttpGet request = new HttpGet(uri); + request.addHeader("Authorization", "token " + this.currentAuthToken); + HttpResponse response = this.httpClient.execute(request); + int responseCode = response.getStatusLine().getStatusCode(); + if (200 == responseCode) { + result.addAll(this.githubMapper.jsonToIssueList(this.getContentOf(response))); + } else if (404 != responseCode) { + String msg = "Error while retrieving issues updated since " + date + " on repo " + repoPath + " : " + responseCode; + log.warn(msg); + throw new IOException(msg); + } + return result; + } + + private boolean isNotEmpty(String str) { + return null != str && !str.trim().isEmpty(); + } + + private String getContentOf(HttpResponse response) throws IOException { + HttpEntity entity = response.getEntity(); + StringBuilder content = new StringBuilder(); + try (BufferedReader contentReader = new BufferedReader(new InputStreamReader(entity.getContent()))) { + String line = contentReader.readLine(); + while (null != line) { + content.append(line); + line = contentReader.readLine(); + } + } + return content.toString(); + } + + private void prepareClient() { + if (null == this.httpClient) { + this.httpClient = HttpClients.createDefault(); + } + } +} diff --git a/server/src/main/java/com/decathlon/ara/service/SettingProviderService.java b/server/src/main/java/com/decathlon/ara/service/SettingProviderService.java index 66f998e61..32989163f 100644 --- a/server/src/main/java/com/decathlon/ara/service/SettingProviderService.java +++ b/server/src/main/java/com/decathlon/ara/service/SettingProviderService.java @@ -1,14 +1,21 @@ package com.decathlon.ara.service; -import com.decathlon.ara.defect.DefectAdapter; import com.decathlon.ara.ci.fetcher.Fetcher; -import com.decathlon.ara.ci.service.FetcherService; import com.decathlon.ara.ci.fetcher.FileSystemFetcher; +import com.decathlon.ara.ci.service.FetcherService; +import com.decathlon.ara.defect.DefectAdapter; import com.decathlon.ara.service.dto.setting.SettingDTO; import com.decathlon.ara.service.dto.setting.SettingGroupDTO; import com.decathlon.ara.service.dto.setting.SettingOptionDTO; import com.decathlon.ara.service.dto.setting.SettingType; import com.decathlon.ara.service.support.Settings; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -18,12 +25,6 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; /** * Service for providing setting definitions. It is ignored from code coverage, as it's merely a configuration file. @@ -586,4 +587,38 @@ public List getDefectRtcDefinitions() { return settings; } + public List getDefectGithubDefinitions() { + List result = new ArrayList<>(); + + result.add(new SettingDTO() + .withCode(Settings.DEFECT_GITHUB_OWNER) + .withName("Github Repository's owner") + .withType(SettingType.STRING) + .withRequired(true) + .withHelp("" + + "The owner of this project's Github repository. Usually the user or organization which " + + "holds the repository.")); + + result.add(new SettingDTO() + .withCode(Settings.DEFECT_GITHUB_REPONAME) + .withName("Github Repository's name") + .withType(SettingType.STRING) + .withRequired(true) + .withHelp("The name of this project's Github repository.")); + + result.add(new SettingDTO() + .withCode(Settings.DEFECT_GITHUB_TOKEN) + .withName("Authorization token") + .withType(SettingType.STRING) + .withRequired(false) + .withHelp("" + + "If your project's repository is a private one, you need to put here the personal token of " + + "a user authorized to read the repository." + + "To create a personal access token, on Github, go to the Settings page of your account, then " + + "click on the 'Developer settings' menu and click on the 'Personal Access Token' menu item. " + + "In this page, generate a new token (enable sso if your organization use it), and copy the " + + "Authorization token displayed in this field.")); + return result; + } + } diff --git a/server/src/main/java/com/decathlon/ara/service/support/Settings.java b/server/src/main/java/com/decathlon/ara/service/support/Settings.java index 26d77ebb8..2b3f9907e 100644 --- a/server/src/main/java/com/decathlon/ara/service/support/Settings.java +++ b/server/src/main/java/com/decathlon/ara/service/support/Settings.java @@ -4,7 +4,7 @@ import lombok.experimental.UtilityClass; /** - * Holds {@link Setting#code}s standard in ARA core application. Other custom adapters are free to provide other ones. + * Holds {@link Setting}'s codes standard in ARA core application. Other custom adapters are free to provide other ones. */ @UtilityClass public class Settings { @@ -27,6 +27,10 @@ public class Settings { public static final String DEFECT_RTC_CLOSED_STATES = "defect.rtc.closedStates"; public static final String DEFECT_RTC_OPEN_STATES = "defect.rtc.openStates"; + public static final String DEFECT_GITHUB_OWNER = "defect.github.owner"; + public static final String DEFECT_GITHUB_REPONAME = "defect.github.repositoryName"; + public static final String DEFECT_GITHUB_TOKEN = "defect.github.authorizationToken"; + public static final String EXECUTION_INDEXER = "execution.indexer"; public static final String EXECUTION_INDEXER_FILE_EXECUTION_BASE_PATH = "execution.indexer.file.executionBasePath"; diff --git a/server/src/main/resources/application-dev.properties b/server/src/main/resources/application-dev.properties index 67774a65e..9c8277b94 100644 --- a/server/src/main/resources/application-dev.properties +++ b/server/src/main/resources/application-dev.properties @@ -1,6 +1,6 @@ # Development environment, to develop/run/test/debug on local machine with a local database -spring.profiles.include=dev_common +spring.profiles.include=dev # The ARA-core development database; to run one ARA per application, override this URL with one schema per application spring.datasource.url=jdbc:mysql://localhost:3306/ara-dev?useUnicode=yes&characterEncoding=UTF-8 diff --git a/server/src/test/java/com/decathlon/ara/defect/github/GithubMapperTest.java b/server/src/test/java/com/decathlon/ara/defect/github/GithubMapperTest.java new file mode 100644 index 000000000..5eeae7281 --- /dev/null +++ b/server/src/test/java/com/decathlon/ara/defect/github/GithubMapperTest.java @@ -0,0 +1,89 @@ +package com.decathlon.ara.defect.github; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.assertj.core.api.Assertions; +import org.assertj.core.util.Lists; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +@RunWith(MockitoJUnitRunner.class) +public class GithubMapperTest { + + @Mock + private ObjectMapper objectMapper; + + @InjectMocks + private GithubMapper cut; + + @Test + public void jsonToIssue_should_return_object() throws IOException { + // Given + String json = "{\"key\": 42 }"; + GithubIssue issue = new GithubIssue(); + issue.setNumber(42); + issue.setTitle("Test"); + issue.setState("open"); + Mockito.doReturn(issue).when(this.objectMapper).readValue(json, GithubMapper.TYPE_REFERENCE_TO_GITHUB_ISSUE); + // When + Optional result = this.cut.jsonToIssue(json); + // Then + Assertions.assertThat(result).isPresent(); + Assertions.assertThat(result.get()).isNotNull(); + Assertions.assertThat(result.get().getNumber()).isEqualTo(42L); + Assertions.assertThat(result.get().getTitle()).isEqualTo("Test"); + Assertions.assertThat(result.get().getState()).isEqualTo("open"); + } + + @Test + public void jsonToIssue_should_return_empty_on_error() throws IOException { + // Given + String json = "{\"key\": 42 }"; + Mockito.doThrow(JsonParseException.class).when(this.objectMapper).readValue(json, GithubMapper.TYPE_REFERENCE_TO_GITHUB_ISSUE); + // When + Optional result = this.cut.jsonToIssue(json); + // Then + Assertions.assertThat(result).isNotPresent(); + } + + @Test + public void jsonToIssueList_should_return_a_filled_list() throws IOException { + // Given + String json = "[ {\"key\": 42 }, {\"key\": 24 } ]"; + GithubIssue issue1 = new GithubIssue(); + issue1.setNumber(42); + issue1.setState("open"); + GithubIssue issue2 = new GithubIssue(); + issue2.setNumber(24); + issue2.setState("closed"); + List issueList = Lists.list(issue1, issue2); + Mockito.doReturn(issueList).when(this.objectMapper).readValue(json, GithubMapper.TYPE_REFERENCE_TO_LIST_GITHUB_ISSUE); + // When + List githubIssues = this.cut.jsonToIssueList(json); + // Then + Assertions.assertThat(githubIssues).isNotNull(); + Assertions.assertThat(githubIssues).hasSize(2); + Assertions.assertThat(githubIssues.get(0).getNumber()).isEqualTo(42); + Assertions.assertThat(githubIssues.get(1).getNumber()).isEqualTo(24); + } + + @Test + public void jsonToIssueList_should_return_an_empty_list_on_error() throws IOException { + // Given + String json = "[ {\"key\": 42 }, {\"key\": 24 } ]"; + Mockito.doThrow(JsonParseException.class).when(this.objectMapper).readValue(json, GithubMapper.TYPE_REFERENCE_TO_LIST_GITHUB_ISSUE); + // When + List githubIssues = this.cut.jsonToIssueList(json); + // Then + Assertions.assertThat(githubIssues).isNotNull(); + Assertions.assertThat(githubIssues).isEmpty(); + } +} \ No newline at end of file diff --git a/server/src/test/java/com/decathlon/ara/defect/github/GithubRestClientTest.java b/server/src/test/java/com/decathlon/ara/defect/github/GithubRestClientTest.java new file mode 100644 index 000000000..735fabf79 --- /dev/null +++ b/server/src/test/java/com/decathlon/ara/defect/github/GithubRestClientTest.java @@ -0,0 +1,237 @@ +package com.decathlon.ara.defect.github; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.StatusLine; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.assertj.core.api.Assertions; +import org.assertj.core.util.Lists; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.*; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Optional; + +@RunWith(MockitoJUnitRunner.class) +public class GithubRestClientTest { + @Mock + private GithubMapper mapper; + + @Mock + private HttpClient httpClient; + + @Spy + @InjectMocks + private GithubRestClient cut; + + + @Test + public void requestIssue_should_return_the_issue() throws IOException, URISyntaxException { + // Given + String owner = "owner"; + String repo = "test"; + String token = "token"; + long issue = 42L; + String jsonResponse = "{\"key\": \"value\"}"; + GithubIssue expectedIssue = new GithubIssue(); + expectedIssue.setNumber(issue); + HttpResponse mockedResponse = this.given_an_issue_response(200, jsonResponse); + Mockito.doReturn(Optional.of(expectedIssue)).when(this.mapper).jsonToIssue(jsonResponse); + Mockito.doReturn(mockedResponse).when(this.httpClient).execute(Mockito.any()); + this.cut.forOwnerAndRepository(owner, repo).withToken(token); + // When + Optional result = this.cut.requestIssue(issue); + // Then + this.assert_that_request_is_well_formed(owner, repo, token, issue); + Assertions.assertThat(result).isPresent(); + Assertions.assertThat(result.get()).isNotNull(); + Assertions.assertThat(result.get().getNumber()).isEqualTo(issue); + } + + @Test + public void requestIssue_should_empty_on_404() throws IOException, URISyntaxException { + // Given + String owner = "owner"; + String repo = "test"; + String token = "token"; + long issue = 42L; + HttpResponse mockedResponse = this.given_an_issue_response(404, "Not found."); + Mockito.doReturn(mockedResponse).when(this.httpClient).execute(Mockito.any()); + this.cut.forOwnerAndRepository(owner, repo).withToken(token); + // When + Optional result = this.cut.requestIssue(issue); + // Then + this.assert_that_request_is_well_formed(owner, repo, token, issue); + Mockito.verify(this.mapper, Mockito.never()).jsonToIssue(Mockito.anyString()); + Assertions.assertThat(result).isNotPresent(); + } + + + @Test + public void requestIssue_should_empty_on_410() throws IOException, URISyntaxException { + // Given + String owner = "owner"; + String repo = "test"; + String token = "token"; + long issue = 42L; + HttpResponse mockedResponse = this.given_an_issue_response(410, "Gone."); + Mockito.doReturn(mockedResponse).when(this.httpClient).execute(Mockito.any()); + this.cut.forOwnerAndRepository(owner, repo).withToken(token); + // When + Optional result = this.cut.requestIssue(issue); + // Then + this.assert_that_request_is_well_formed(owner, repo, token, issue); + Mockito.verify(this.mapper, Mockito.never()).jsonToIssue(Mockito.anyString()); + Assertions.assertThat(result).isNotPresent(); + } + + @Test + public void requestIssue_should_throw_exception_on_500() throws IOException { + // Given + String owner = "owner"; + String repo = "test"; + String token = "token"; + long issue = 42L; + HttpResponse mockedResponse = this.given_an_issue_response(500, "Internal Server Error."); + Mockito.doReturn(mockedResponse).when(this.httpClient).execute(Mockito.any()); + this.cut.forOwnerAndRepository(owner, repo).withToken(token); + // When + try { + this.cut.requestIssue(issue); + Assertions.fail("IOException is expected on error 500."); + } catch (IOException | URISyntaxException ex) { + String expectedMessage = "Error while requesting issue " + issue + " on repo " + + owner + "/" + repo + " : 500"; + Assertions.assertThat(ex.getMessage()).isEqualTo(expectedMessage); + } + } + + @Test + public void requestIssues_should_request_all_the_issues() throws IOException, URISyntaxException { + // Given + String owner = "owner"; + String repo = "test"; + String token = "token"; + List issueIds = Lists.list(1L, 2L, 3L, 4L, 5L, 6L, 7L); + this.cut.forOwnerAndRepository(owner, repo).withToken(token); + Mockito.doReturn(Optional.of(new GithubIssue())).when(this.cut).requestIssue(Mockito.anyLong()); + // When + this.cut.requestIssues(issueIds); + // Then + Mockito.verify(this.cut, Mockito.times(7)).requestIssue(Mockito.anyLong()); + } + + @Test + public void getIssuesUpdatedSince_should_return_the_list_of_issues_after_date() throws IOException, URISyntaxException, ParseException { + // Given + String owner = "owner"; + String repo = "test"; + String token = "token"; + DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + String expectedDate = "2019-04-04T04:21:00"; + Date date = format.parse(expectedDate); + String jsonResponse = "[ {\"key\": \"value\"}, {\"key\": \"value2\" } ]"; + GithubIssue issue1 = new GithubIssue(); + issue1.setNumber(42L); + GithubIssue issue2 = new GithubIssue(); + issue2.setNumber(24L); + HttpResponse mockedResponse = this.given_an_issue_response(200, jsonResponse); + Mockito.doReturn(Lists.list(issue1, issue2)).when(this.mapper).jsonToIssueList(jsonResponse); + Mockito.doReturn(mockedResponse).when(this.httpClient).execute(Mockito.any()); + this.cut.forOwnerAndRepository(owner, repo).withToken(token); + // When + List issuesUpdatedSince = this.cut.getIssuesUpdatedSince(date); + // Then + this.assert_that_issue_since_request_is_well_formed(owner, repo, expectedDate); + Assertions.assertThat(issuesUpdatedSince).isNotNull(); + Assertions.assertThat(issuesUpdatedSince).hasSize(2); + Assertions.assertThat(issuesUpdatedSince).containsExactly(issue1, issue2); + } + + @Test + public void getIssuesUpdatedSince_should_return_empty_list_on_404() throws IOException, URISyntaxException, ParseException { + // Given + String owner = "owner"; + String repo = "test"; + DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + String expectedDate = "2019-04-04T04:21:00"; + Date date = format.parse(expectedDate); + String contentReponse = "Not Found."; + HttpResponse mockedResponse = this.given_an_issue_response(404, contentReponse); + Mockito.doReturn(mockedResponse).when(this.httpClient).execute(Mockito.any()); + this.cut.forOwnerAndRepository(owner, repo); + // When + List issuesUpdatedSince = this.cut.getIssuesUpdatedSince(date); + // Then + this.assert_that_issue_since_request_is_well_formed(owner, repo, expectedDate); + Assertions.assertThat(issuesUpdatedSince).isNotNull(); + Assertions.assertThat(issuesUpdatedSince).isEmpty(); + } + + @Test + public void getIssuesUpdatedSince_should_throw_error_on_500() throws IOException, URISyntaxException, ParseException { + // Given + String owner = "owner"; + String repo = "test"; + String token = "token"; + DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + String expectedDate = "2019-04-04T04:21:00"; + Date date = format.parse(expectedDate); + String contentReponse = "Not Found."; + HttpResponse mockedResponse = this.given_an_issue_response(500, contentReponse); + Mockito.doReturn(mockedResponse).when(this.httpClient).execute(Mockito.any()); + this.cut.forOwnerAndRepository(owner, repo).withToken(token); + // When + try { + this.cut.getIssuesUpdatedSince(date); + Assertions.fail("An IOException was expected here."); + } catch (IOException ex) { + Assertions.assertThat(ex.getMessage()).isEqualTo("Error while retrieving issues updated since " + + expectedDate + " on repo " + owner + "/" + repo + " : 500"); + } + } + + private HttpResponse given_an_issue_response(int code, String body) throws IOException { + HttpResponse response = Mockito.mock(HttpResponse.class); + // Status Line + StatusLine statusLine = Mockito.mock(StatusLine.class); + Mockito.doReturn(code).when(statusLine).getStatusCode(); + Mockito.doReturn(statusLine).when(response).getStatusLine(); + // Entity + HttpEntity entity = Mockito.mock(HttpEntity.class); + Mockito.doReturn(new ByteArrayInputStream(body.getBytes())).when(entity).getContent(); + Mockito.doReturn(entity).when(response).getEntity(); + return response; + } + + private void assert_that_request_is_well_formed(String owner, String repo, String token, long issue) throws IOException, URISyntaxException { + ArgumentCaptor request = ArgumentCaptor.forClass(HttpGet.class); + Mockito.verify(this.httpClient).execute(request.capture()); + String expectedPath = GithubRestClient.PROTOCOL + "://" + GithubRestClient.BASEPATH + + "/repos/" + owner + "/" + repo + "/issues/" + issue; + expectedPath += "?filter=all&state=all"; + Assertions.assertThat(request.getValue().getURI()).isEqualTo(new URI(expectedPath)); + Assertions.assertThat(request.getValue().containsHeader("Authorization")).isTrue(); + } + + private void assert_that_issue_since_request_is_well_formed(String owner, String repo, String time) throws IOException, URISyntaxException { + ArgumentCaptor request = ArgumentCaptor.forClass(HttpGet.class); + Mockito.verify(this.httpClient).execute(request.capture()); + String expectedPath = GithubRestClient.PROTOCOL + "://" + GithubRestClient.BASEPATH + + "/repos/" + owner + "/" + repo + "/issues"; + expectedPath += "?filter=all&state=all&since=" + time.replace(":", "%3A"); + Assertions.assertThat(request.getValue().getURI()).isEqualTo(new URI(expectedPath)); + Assertions.assertThat(request.getValue().containsHeader("Authorization")).isTrue(); + } +} \ No newline at end of file