View Javadoc
1   package com.acumenvelocity.ath.integration;
2   
3   import java.io.BufferedReader;
4   import java.io.BufferedWriter;
5   import java.io.IOException;
6   import java.io.InputStreamReader;
7   import java.io.OutputStream;
8   import java.io.OutputStreamWriter;
9   import java.net.HttpURLConnection;
10  import java.net.URL;
11  import java.nio.charset.StandardCharsets;
12  import java.nio.file.Files;
13  import java.nio.file.Path;
14  
15  import org.junit.jupiter.api.BeforeAll;
16  import org.junit.jupiter.api.TestInstance;
17  
18  import com.fasterxml.jackson.databind.JsonNode;
19  import com.fasterxml.jackson.databind.ObjectMapper;
20  import com.fasterxml.jackson.databind.SerializationFeature;
21  import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
22  
23  @TestInstance(TestInstance.Lifecycle.PER_CLASS)
24  public abstract class BaseIntegrationTest {
25    protected static final String BASE_URL = "http://localhost:8080/api";
26    protected static final String SOLR_URL = "http://localhost:8983/solr";
27    protected static final ObjectMapper objectMapper = new ObjectMapper()
28        .registerModule(new JavaTimeModule())
29        .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
30  
31  //  private static Process dockerComposeProcess;
32    private static final int MAX_STARTUP_WAIT_SECONDS = 120;
33  
34  //  @BeforeAll
35  //  public void setUpDockerEnvironment() throws Exception {
36  //    System.out.println("Starting Docker Compose environment...");
37  //    System.out.println("This may take a few minutes on first run (building images)...\n");
38  //
39  //    // Get the docker-compose.yml path
40  //    Path dockerComposePath = Paths.get("./docker/docker-compose.yml").toAbsolutePath()
41  //        .normalize();
42  //
43  //    if (!Files.exists(dockerComposePath)) {
44  //      throw new FileNotFoundException("docker-compose.yml not found at: " + dockerComposePath);
45  //    }
46  //
47  //    // Start docker-compose with output redirected to console
48  //    ProcessBuilder processBuilder = new ProcessBuilder(
49  //        "docker",
50  //        "compose",
51  //        "-f", dockerComposePath.toString(),
52  //        "up",
53  //        "-d",
54  //        "--build",
55  //        "--remove-orphans");
56  //
57  //    processBuilder.directory(dockerComposePath.getParent().toFile());
58  //
59  //    // Redirect output to console instead of consuming it
60  //    processBuilder.inheritIO();
61  //
62  //    System.out.println("Running: " + String.join(" ", processBuilder.command()));
63  //    System.out.println("Working directory: " + processBuilder.directory());
64  //    System.out.println();
65  //
66  //    dockerComposeProcess = processBuilder.start();
67  //    int exitCode = dockerComposeProcess.waitFor();
68  //
69  //    if (exitCode != 0) {
70  //      throw new RuntimeException("Docker Compose failed to start (exit code: " + exitCode + ")");
71  //    }
72  //
73  //    System.out
74  //        .println("\nDocker Compose containers started, waiting for services to be healthy...");
75  //
76  //    // Wait for services to be healthy
77  //    waitForServices();
78  //
79  //    System.out.println("Docker Compose environment is ready\n");
80  //  }
81    
82    @BeforeAll
83    public void setUpDockerEnvironment() throws Exception {
84      System.out.println("Docker Compose must be started manually");
85      System.out.println("This may take a few minutes on first run (building images)...\n");
86  
87      // Wait for services to be healthy
88      waitForServices();
89  
90      System.out.println("Docker Compose environment is ready\n");
91    }
92  
93    // @AfterAll
94    // public void tearDownDockerEnvironment() throws Exception {
95    // System.out.println("\nStopping Docker Compose environment...");
96    //
97    // Path dockerComposePath = Paths.get("./docker/docker-compose.yml").toAbsolutePath()
98    // .normalize();
99    //
100   // ProcessBuilder processBuilder = new ProcessBuilder(
101   // "docker",
102   // "compose",
103   // "-f", dockerComposePath.toString(),
104   // "down",
105   // "-v");
106   //
107   // processBuilder.directory(dockerComposePath.getParent().toFile());
108   // processBuilder.inheritIO();
109   //
110   // Process process = processBuilder.start();
111   // process.waitFor(30, TimeUnit.SECONDS);
112   //
113   // if (dockerComposeProcess != null && dockerComposeProcess.isAlive()) {
114   // dockerComposeProcess.destroy();
115   // }
116   //
117   // System.out.println("Docker Compose environment stopped");
118   // }
119 
120   private void waitForServices() throws Exception {
121     System.out.println("Waiting for services to be healthy...");
122     System.out.println("Checking: " + BASE_URL + "/version");
123     System.out.println("Checking: " + SOLR_URL + "/admin/info/system");
124 
125     long startTime = System.currentTimeMillis();
126     long timeout = MAX_STARTUP_WAIT_SECONDS * 1000;
127     int attemptCount = 0;
128 
129     while (System.currentTimeMillis() - startTime < timeout) {
130       attemptCount++;
131       boolean apiHealthy = isServiceHealthy(BASE_URL + "/version");
132       boolean solrHealthy = isServiceHealthy(SOLR_URL + "/admin/info/system");
133 
134       if (apiHealthy && solrHealthy) {
135         System.out.println("\n✓ All services are healthy after " + attemptCount + " attempts");
136         return;
137       }
138 
139       // Print status every 5 attempts
140       if (attemptCount % 5 == 0) {
141         System.out.print(".");
142         System.out.flush();
143       }
144 
145       // Print detailed status every 15 attempts
146       if (attemptCount % 15 == 0) {
147         long elapsed = (System.currentTimeMillis() - startTime) / 1000;
148         System.out.println();
149         System.out.println("  [" + elapsed + "s] API: " + (apiHealthy ? "✓" : "✗") +
150             ", Solr: " + (solrHealthy ? "✓" : "✗"));
151       }
152 
153       Thread.sleep(2000);
154     }
155 
156     throw new RuntimeException(
157         "Services did not become healthy within " + MAX_STARTUP_WAIT_SECONDS + " seconds");
158   }
159 
160   private boolean isServiceHealthy(String urlString) {
161     try {
162       URL url = new URL(urlString);
163       HttpURLConnection connection = (HttpURLConnection) url.openConnection();
164       connection.setRequestMethod("GET");
165       connection.setConnectTimeout(5000);
166       connection.setReadTimeout(5000);
167 
168       int responseCode = connection.getResponseCode();
169       connection.disconnect();
170 
171       return responseCode == 200;
172     } catch (Exception e) {
173       return false;
174     }
175   }
176 
177   // HTTP Helper Methods
178 
179   protected HttpResponse sendGetRequest(String endpoint) throws IOException {
180     return sendRequest("GET", endpoint, null, null);
181   }
182 
183   protected HttpResponse sendPostRequest(String endpoint, String jsonBody) throws IOException {
184     return sendRequest("POST", endpoint, jsonBody, "application/json");
185   }
186 
187   protected HttpResponse sendPutRequest(String endpoint, String jsonBody) throws IOException {
188     return sendRequest("PUT", endpoint, jsonBody, "application/json");
189   }
190 
191   protected HttpResponse sendDeleteRequest(String endpoint) throws IOException {
192     return sendRequest("DELETE", endpoint, null, null);
193   }
194 
195   protected HttpResponse sendMultipartRequest(String endpoint, MultipartRequestBody body)
196       throws IOException {
197     String boundary = "----IntegrationTestBoundary" + System.currentTimeMillis();
198 
199     URL url = new URL(BASE_URL + endpoint);
200     HttpURLConnection connection = (HttpURLConnection) url.openConnection();
201     connection.setRequestMethod("POST");
202     connection.setDoOutput(true);
203     connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
204 
205     try (OutputStream out = connection.getOutputStream();
206         BufferedWriter writer = new BufferedWriter(
207             new OutputStreamWriter(out, StandardCharsets.UTF_8))) {
208 
209       for (MultipartRequestBody.Part part : body.getParts()) {
210         writer.write("--" + boundary + "\r\n");
211 
212         if (part.isFile()) {
213           writer.write("Content-Disposition: form-data; name=\"" + part.getName() +
214               "\"; filename=\"" + part.getFileName() + "\"\r\n");
215           writer.write("Content-Type: " + part.getContentType() + "\r\n\r\n");
216           writer.flush();
217 
218           Files.copy(part.getFilePath(), out);
219           writer.write("\r\n");
220         } else {
221           writer.write("Content-Disposition: form-data; name=\"" + part.getName() + "\"\r\n\r\n");
222           writer.write(part.getValue() + "\r\n");
223         }
224       }
225 
226       writer.write("--" + boundary + "--\r\n");
227       writer.flush();
228     }
229 
230     return getResponse(connection);
231   }
232 
233   protected HttpResponse sendPutMultipartRequest(String endpoint, MultipartRequestBody body)
234       throws IOException {
235     String boundary = "----IntegrationTestBoundary" + System.currentTimeMillis();
236 
237     URL url = new URL(BASE_URL + endpoint);
238     HttpURLConnection connection = (HttpURLConnection) url.openConnection();
239     connection.setRequestMethod("PUT");
240     connection.setDoOutput(true);
241     connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
242 
243     try (OutputStream out = connection.getOutputStream();
244         BufferedWriter writer = new BufferedWriter(
245             new OutputStreamWriter(out, StandardCharsets.UTF_8))) {
246 
247       for (MultipartRequestBody.Part part : body.getParts()) {
248         writer.write("--" + boundary + "\r\n");
249 
250         if (part.isFile()) {
251           writer.write("Content-Disposition: form-data; name=\"" + part.getName() +
252               "\"; filename=\"" + part.getFileName() + "\"\r\n");
253           writer.write("Content-Type: " + part.getContentType() + "\r\n\r\n");
254           writer.flush();
255 
256           Files.copy(part.getFilePath(), out);
257           writer.write("\r\n");
258         } else {
259           writer.write("Content-Disposition: form-data; name=\"" + part.getName() + "\"\r\n\r\n");
260           writer.write(part.getValue() + "\r\n");
261         }
262       }
263 
264       writer.write("--" + boundary + "--\r\n");
265       writer.flush();
266     }
267 
268     return getResponse(connection);
269   }
270 
271   private HttpResponse sendRequest(String method, String endpoint, String body, String contentType)
272       throws IOException {
273     URL url = new URL(BASE_URL + endpoint);
274     HttpURLConnection connection = (HttpURLConnection) url.openConnection();
275     connection.setRequestMethod(method);
276 
277     if (body != null && contentType != null) {
278       connection.setDoOutput(true);
279       connection.setRequestProperty("Content-Type", contentType);
280 
281       try (OutputStream os = connection.getOutputStream()) {
282         os.write(body.getBytes(StandardCharsets.UTF_8));
283       }
284     }
285 
286     return getResponse(connection);
287   }
288 
289   private HttpResponse getResponse(HttpURLConnection connection) throws IOException {
290     int statusCode = connection.getResponseCode();
291     String responseBody;
292 
293     try (BufferedReader br = new BufferedReader(
294         new InputStreamReader(
295             statusCode >= 400 ? connection.getErrorStream() : connection.getInputStream(),
296             StandardCharsets.UTF_8))) {
297       StringBuilder response = new StringBuilder();
298       String line;
299       while ((line = br.readLine()) != null) {
300         response.append(line);
301       }
302       responseBody = response.toString();
303     }
304 
305     connection.disconnect();
306     return new HttpResponse(statusCode, responseBody);
307   }
308 
309   // Utility Methods
310 
311   protected JsonNode parseJson(String json) throws IOException {
312     return objectMapper.readTree(json);
313   }
314 
315   protected String toJson(Object obj) throws IOException {
316     return objectMapper.writeValueAsString(obj);
317   }
318 
319   protected void waitForAsyncOperation(String statusEndpoint, int maxWaitSeconds) throws Exception {
320     long startTime = System.currentTimeMillis();
321     long timeout = maxWaitSeconds * 1000;
322 
323     while (System.currentTimeMillis() - startTime < timeout) {
324       HttpResponse response = sendGetRequest(statusEndpoint);
325 
326       if (response.getStatusCode() == 200) {
327         JsonNode json = parseJson(response.getBody());
328         String status = json.get("status").asText();
329 
330         if (status.equals("IMPORT_COMPLETED") || status.equals("EXPORT_COMPLETED")) {
331           return;
332         } else if (status.equals("FAILED")) {
333           throw new RuntimeException("Operation failed: " + json.get("error_message").asText());
334         }
335       }
336 
337       Thread.sleep(1000);
338     }
339 
340     throw new RuntimeException("Operation did not complete within " + maxWaitSeconds + " seconds");
341   }
342 
343   // Inner Classes
344 
345   protected static class HttpResponse {
346     private final int statusCode;
347     private final String body;
348 
349     public HttpResponse(int statusCode, String body) {
350       this.statusCode = statusCode;
351       this.body = body;
352     }
353 
354     public int getStatusCode() {
355       return statusCode;
356     }
357 
358     public String getBody() {
359       return body;
360     }
361   }
362 
363   protected static class MultipartRequestBody {
364     private final java.util.List<Part> parts = new java.util.ArrayList<>();
365 
366     public void addPart(String name, String value) {
367       parts.add(new Part(name, value));
368     }
369 
370     public void addFilePart(String name, String fileName, Path filePath, String contentType) {
371       parts.add(new Part(name, fileName, filePath, contentType));
372     }
373 
374     public java.util.List<Part> getParts() {
375       return parts;
376     }
377 
378     protected static class Part {
379       private final String name;
380       private final String value;
381       private final String fileName;
382       private final Path filePath;
383       private final String contentType;
384       private final boolean isFile;
385 
386       public Part(String name, String value) {
387         this.name = name;
388         this.value = value;
389         this.fileName = null;
390         this.filePath = null;
391         this.contentType = null;
392         this.isFile = false;
393       }
394 
395       public Part(String name, String fileName, Path filePath, String contentType) {
396         this.name = name;
397         this.value = null;
398         this.fileName = fileName;
399         this.filePath = filePath;
400         this.contentType = contentType;
401         this.isFile = true;
402       }
403 
404       public String getName() {
405         return name;
406       }
407 
408       public String getValue() {
409         return value;
410       }
411 
412       public String getFileName() {
413         return fileName;
414       }
415 
416       public Path getFilePath() {
417         return filePath;
418       }
419 
420       public String getContentType() {
421         return contentType;
422       }
423 
424       public boolean isFile() {
425         return isFile;
426       }
427     }
428   }
429 }