View Javadoc
1   /*
2    * Copyright 2023 Adrian Herscu
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  // see https://support.gurock.com/hc/en-us/articles/7077074577044-Binding-Java#01G68HHNMFWZVK25DD1SX36NK7
17  package dev.aherscu.qa.testrail.reporter;
18  
19  import static java.net.HttpURLConnection.*;
20  
21  import java.io.*;
22  import java.net.*;
23  import java.nio.charset.*;
24  import java.util.*;
25  
26  import org.json.simple.*;
27  
28  import lombok.*;
29  
30  public class TestRailClient {
31  
32      private final String m_url;
33      private String       m_user;
34      private String       m_password;
35  
36      public TestRailClient(String base_url) {
37          if (!base_url.endsWith("/")) {
38              base_url += "/";
39          }
40  
41          this.m_url = base_url + "index.php?/api/v2/";
42      }
43  
44      private static String getAuthorization(String user, String password) {
45          return new String(
46              Base64.getEncoder().encode((user + ":" + password).getBytes()));
47      }
48  
49      /**
50       * Get/Set Password
51       * <p>
52       * Returns/sets the password used for authenticating the API requests.
53       *
54       * @return the password
55       */
56      public String getPassword() {
57          return this.m_password;
58      }
59  
60      public void setPassword(String password) {
61          this.m_password = password;
62      }
63  
64      /**
65       * Get/Set User
66       * <p>
67       * Returns/sets the user used for authenticating the API requests.
68       *
69       * @return the user
70       */
71      public String getUser() {
72          return this.m_user;
73      }
74  
75      public void setUser(String user) {
76          this.m_user = user;
77      }
78  
79      /**
80       * Send Get
81       * <p>
82       * Issues a GET request (read) against the API and returns the result (as
83       * Object, see below).
84       * <p>
85       * Arguments:
86       * <p>
87       * uri The API method to call including parameters (e.g. get_case/1)
88       * <p>
89       * Returns the parsed JSON response as standard object which can either be
90       * an instance of JSONObject or JSONArray (depending on the API method). In
91       * most cases, this returns a JSONObject instance which is basically the
92       * same as java.util.Map.
93       * <p>
94       * If 'get_attachment/:attachment_id', returns a String
95       *
96       * @param uri
97       *            uri
98       * @param data
99       *            data
100      * @return the response
101      */
102     public Object sendGet(String uri, String data) {
103         return this.sendRequest("GET", uri, data);
104     }
105 
106     public Object sendGet(String uri) {
107         return this.sendRequest("GET", uri, null);
108     }
109 
110     /**
111      * Send POST
112      * <p>
113      * Issues a POST request (write) against the API and returns the result (as
114      * Object, see below).
115      * <p>
116      * Arguments:
117      * <p>
118      * uri The API method to call including parameters (e.g. add_case/1) data
119      * The data to submit as part of the request (e.g., a map) If adding an
120      * attachment, must be the path to the file
121      * <p>
122      * Returns the parsed JSON response as standard object which can either be
123      * an instance of JSONObject or JSONArray (depending on the API method). In
124      * most cases, this returns a JSONObject instance which is basically the
125      * same as java.util.Map.
126      *
127      * @param uri
128      *            uri
129      * @param data
130      *            data
131      * @return the response
132      */
133     public Object sendPost(String uri, Object data) {
134         return this.sendRequest("POST", uri, data);
135     }
136 
137     @SneakyThrows
138     private Object sendRequest(String method, String uri, Object data) {
139         URL url = new URL(this.m_url + uri);
140         // Create the connection object and set the required HTTP method
141         // (GET/POST) and headers (content type and basic auth).
142         HttpURLConnection conn = (HttpURLConnection) url.openConnection();
143 
144         String auth = getAuthorization(this.m_user, this.m_password);
145         conn.addRequestProperty("Authorization", "Basic " + auth);
146 
147         if (method.equals("POST")) {
148             conn.setRequestMethod("POST");
149             // Add the POST arguments, if any. We just serialize the passed
150             // data object (i.e. a dictionary) and then add it to the
151             // request body.
152             if (data != null) {
153                 if (uri.startsWith(
154                     "add_attachment")) // add_attachment API requests
155                 {
156                     String boundary =
157                         "TestRailAPIAttachmentBoundary"; // Can be any random
158                                                          // string
159                     File uploadFile = new File((String) data);
160 
161                     conn.setDoOutput(true);
162                     conn.addRequestProperty("Content-Type",
163                         "multipart/form-data; boundary=" + boundary);
164 
165                     OutputStream ostreamBody = conn.getOutputStream();
166                     BufferedWriter bodyWriter =
167                         new BufferedWriter(new OutputStreamWriter(ostreamBody));
168 
169                     bodyWriter.write("\n\n--" + boundary + "\r\n");
170                     bodyWriter.write(
171                         "Content-Disposition: form-data; name=\"attachment\"; filename=\""
172                             + uploadFile.getName() + "\"");
173                     bodyWriter.write("\r\n\r\n");
174                     bodyWriter.flush();
175 
176                     // Read file into request
177                     InputStream istreamFile = new FileInputStream(uploadFile);
178                     int bytesRead;
179                     byte[] dataBuffer = new byte[1024];
180                     while ((bytesRead = istreamFile.read(dataBuffer)) != -1) {
181                         ostreamBody.write(dataBuffer, 0, bytesRead);
182                     }
183 
184                     ostreamBody.flush();
185 
186                     // end of attachment, add boundary
187                     bodyWriter.write("\r\n--" + boundary + "--\r\n");
188                     bodyWriter.flush();
189 
190                     // Close streams
191                     istreamFile.close();
192                     ostreamBody.close();
193                     bodyWriter.close();
194                 } else // Not an attachment
195                 {
196                     conn.addRequestProperty("Content-Type", "application/json");
197                     byte[] block =
198                         JSONValue.toJSONString(data).getBytes(
199                             StandardCharsets.UTF_8);
200 
201                     conn.setDoOutput(true);
202                     OutputStream ostream = conn.getOutputStream();
203                     ostream.write(block);
204                     ostream.close();
205                 }
206             }
207         } else // GET request
208         {
209             conn.addRequestProperty("Content-Type", "application/json");
210         }
211 
212         // Execute the actual web request (if it wasn't already initiated
213         // by getOutputStream above) and record any occurred errors (we use
214         // the error stream in this case).
215         int status = conn.getResponseCode();
216 
217         InputStream istream;
218         if (status != HTTP_OK) {
219             istream = conn.getErrorStream();
220             if (istream == null) {
221                 throw new RuntimeException(
222                     "TestRail API return HTTP " + status +
223                         " (No additional error message received)");
224             }
225         } else {
226             istream = conn.getInputStream();
227         }
228 
229         // If 'get_attachment' (not 'get_attachments') returned valid status
230         // code, save the file
231         if ((istream != null)
232             && (uri.startsWith("get_attachment/"))) {
233             FileOutputStream outputStream = new FileOutputStream((String) data);
234 
235             int bytesRead = 0;
236             byte[] buffer = new byte[1024];
237             while ((bytesRead = istream.read(buffer)) > 0) {
238                 outputStream.write(buffer, 0, bytesRead);
239             }
240 
241             outputStream.close();
242             istream.close();
243             return data;
244         }
245 
246         // Not an attachment received
247         // Read the response body, if any, and deserialize it from JSON.
248         String text = "";
249         if (istream != null) {
250             BufferedReader reader = new BufferedReader(
251                 new InputStreamReader(
252                     istream,
253                     StandardCharsets.UTF_8));
254 
255             String line;
256             while ((line = reader.readLine()) != null) {
257                 text += line;
258                 text += System.getProperty("line.separator");
259             }
260 
261             reader.close();
262         }
263 
264         Object result;
265         if (!text.equals("")) {
266             result = JSONValue.parse(text);
267         } else {
268             result = new JSONObject();
269         }
270 
271         // Check for any occurred errors and add additional details to
272         // the exception message, if any (e.g. the error message returned
273         // by TestRail).
274         if (status != HTTP_OK) {
275             String error = "No additional error message received";
276             if (result instanceof JSONObject) {
277                 JSONObject obj = (JSONObject) result;
278                 if (obj.containsKey("error")) {
279                     error = '"' + (String) obj.get("error") + '"';
280                 }
281             }
282 
283             throw new RuntimeException(
284                 "TestRail API returned HTTP " + status +
285                     "(" + error + ")");
286         }
287 
288         return result;
289     }
290 }