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
32 private static final int MAX_STARTUP_WAIT_SECONDS = 120;
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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
88 waitForServices();
89
90 System.out.println("Docker Compose environment is ready\n");
91 }
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
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
140 if (attemptCount % 5 == 0) {
141 System.out.print(".");
142 System.out.flush();
143 }
144
145
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
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
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
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 }