1 package com.guinetik.rr;
2
3 import com.fasterxml.jackson.databind.ObjectMapper;
4 import com.guinetik.rr.api.ApiException;
5 import com.guinetik.rr.http.*;
6 import com.guinetik.rr.request.RequestSpec;
7 import com.guinetik.rr.result.ApiError;
8 import com.guinetik.rr.result.Result;
9 import org.slf4j.Logger;
10 import org.slf4j.LoggerFactory;
11
12 import java.util.HashMap;
13 import java.util.Map;
14 import java.util.concurrent.CompletableFuture;
15 import java.util.concurrent.ConcurrentHashMap;
16 import java.util.function.BiFunction;
17 import java.util.regex.Pattern;
18 import java.util.regex.Matcher;
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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
83
84
85
86
87
88
89
90
91
92
93
94
95 public class RocketRestMock extends RocketRest {
96 private final Logger logger = LoggerFactory.getLogger(this.getClass());
97 private final ObjectMapper objectMapper = new ObjectMapper();
98
99
100 private final Map<String, MockResponseDefinition> mockResponses = new HashMap<>();
101
102
103 private final ConcurrentHashMap<String, Integer> invocationCounts = new ConcurrentHashMap<>();
104
105
106 private final Map<Pattern, Long> latencySettings = new HashMap<>();
107
108
109 private final MockRocketClient mockClient;
110
111
112
113
114
115
116 public RocketRestMock(RocketRestConfig config) {
117 super(config);
118
119
120 this.mockClient = new MockRocketClient();
121
122
123 if (config.getDefaultOptions().getBoolean(HttpConstants.CircuitBreaker.CIRCUIT_BREAKER_ENABLED, false)) {
124 int failureThreshold = config.getDefaultOptions().getInt(
125 HttpConstants.CircuitBreaker.CIRCUIT_BREAKER_FAILURE_THRESHOLD,
126 HttpConstants.CircuitBreaker.DEFAULT_FAILURE_THRESHOLD
127 );
128 long resetTimeoutMs = config.getDefaultOptions().getLong(
129 HttpConstants.CircuitBreaker.CIRCUIT_BREAKER_RESET_TIMEOUT_MS,
130 HttpConstants.CircuitBreaker.DEFAULT_RESET_TIMEOUT_MS
131 );
132
133
134 CircuitBreakerClient circuitBreakerClient;
135
136
137 String policyStr = config.getDefaultOptions().getString(
138 HttpConstants.CircuitBreaker.CIRCUIT_BREAKER_FAILURE_POLICY,
139 null
140 );
141 if (HttpConstants.CircuitBreaker.CIRCUIT_BREAKER_POLICY_SERVER_ONLY.equals(policyStr)) {
142 circuitBreakerClient = new CircuitBreakerClient(
143 mockClient,
144 failureThreshold,
145 resetTimeoutMs,
146 HttpConstants.CircuitBreaker.DEFAULT_FAILURE_DECAY_TIME_MS,
147 CircuitBreakerClient.FailurePolicy.SERVER_ERRORS_ONLY,
148 null
149 );
150 } else {
151 circuitBreakerClient = new CircuitBreakerClient(
152 mockClient,
153 failureThreshold,
154 resetTimeoutMs
155 );
156 }
157
158
159 this.mockClient.setDelegate(circuitBreakerClient);
160 }
161 }
162
163
164
165
166
167 private static class MockResponseDefinition {
168 final BiFunction<String, Object, Object> responseProducer;
169 final boolean isRegexPattern;
170 final Pattern compiledPattern;
171
172 MockResponseDefinition(BiFunction<String, Object, Object> responseProducer, boolean isRegexPattern, String pattern) {
173 this.responseProducer = responseProducer;
174 this.isRegexPattern = isRegexPattern;
175 this.compiledPattern = isRegexPattern ? Pattern.compile(pattern) : null;
176 }
177 }
178
179
180
181
182
183
184
185
186 public void addMockResponse(String method, String urlPattern,
187 BiFunction<String, Object, Object> responseProducer) {
188 addMockResponse(method, urlPattern, responseProducer, false);
189 }
190
191
192
193
194
195
196
197
198
199 public void addMockResponse(String method, String urlPattern,
200 BiFunction<String, Object, Object> responseProducer,
201 boolean isRegexPattern) {
202 String key = method + ":" + urlPattern;
203 mockResponses.put(key, new MockResponseDefinition(responseProducer, isRegexPattern, urlPattern));
204 logger.debug("Added mock response for {} {} (regex: {})", method, urlPattern, isRegexPattern);
205 }
206
207
208
209
210
211 private BiFunction<String, Object, Object> findMatchingResponse(String method, String url) {
212
213 String exactKey = method + ":" + url;
214
215
216 MockResponseDefinition exactMatch = mockResponses.get(exactKey);
217 if (exactMatch != null) {
218
219 trackInvocation(method, url);
220 return exactMatch.responseProducer;
221 }
222
223
224 for (Map.Entry<String, MockResponseDefinition> entry : mockResponses.entrySet()) {
225 String key = entry.getKey();
226 MockResponseDefinition def = entry.getValue();
227
228
229 if (!def.isRegexPattern || !key.startsWith(method + ":")) {
230 continue;
231 }
232
233
234 Matcher matcher = def.compiledPattern.matcher(url);
235 if (matcher.matches()) {
236
237 trackInvocation(method, key.substring(method.length() + 1));
238 return def.responseProducer;
239 }
240 }
241
242 return null;
243 }
244
245
246
247
248 private void trackInvocation(String method, String urlPattern) {
249 String key = method + ":" + urlPattern;
250 invocationCounts.compute(key, (k, v) -> (v == null) ? 1 : v + 1);
251 }
252
253
254
255
256
257
258
259
260 public int getInvocationCount(String method, String urlPattern) {
261 String key = method + ":" + urlPattern;
262 return invocationCounts.getOrDefault(key, 0);
263 }
264
265
266
267
268 public void resetInvocationCounts() {
269 invocationCounts.clear();
270 }
271
272
273
274
275
276
277
278 public void withLatency(String urlPatternRegex, long latencyMs) {
279 latencySettings.put(Pattern.compile(urlPatternRegex), latencyMs);
280 logger.debug("Added latency of {}ms for URL pattern: {}", latencyMs, urlPatternRegex);
281 }
282
283
284
285
286 private void simulateLatency(String url) {
287 for (Map.Entry<Pattern, Long> entry : latencySettings.entrySet()) {
288 if (entry.getKey().matcher(url).matches()) {
289 long latency = entry.getValue();
290 logger.debug("Simulating latency of {}ms for URL: {}", latency, url);
291 try {
292 Thread.sleep(latency);
293 } catch (InterruptedException e) {
294 Thread.currentThread().interrupt();
295 }
296 return;
297 }
298 }
299 }
300
301 @Override
302 public <Req, Res> Res execute(RequestSpec<Req, Res> requestSpec) {
303
304 simulateLatency(requestSpec.getEndpoint());
305
306
307 return mockClient.execute(requestSpec);
308 }
309
310
311
312
313 private <Req, Res> Result<Res, ApiError> executeWithResult(RequestSpec<Req, Res> requestSpec) {
314 try {
315 Res result = execute(requestSpec);
316 return Result.success(result);
317 } catch (CircuitBreakerOpenException e) {
318 return Result.failure(ApiError.circuitOpenError(e.getMessage()));
319 } catch (RocketRestException e) {
320 return Result.failure(ApiError.httpError(e.getMessage(), e.getStatusCode(), e.getResponseBody()));
321 } catch (Exception e) {
322 return Result.failure(ApiError.networkError(e.getMessage()));
323 }
324 }
325
326
327
328
329 @Override
330 public SyncApi sync() {
331 return new MockSyncApi();
332 }
333
334
335
336
337 @Override
338 public AsyncApi async() {
339 return new MockAsyncApi();
340 }
341
342
343
344
345 public FluentApi fluent() {
346 return new MockFluentApi();
347 }
348
349 public <Req, Res> CompletableFuture<Res> executeAsync(RequestSpec<Req, Res> requestSpec) {
350 return CompletableFuture.supplyAsync(() -> execute(requestSpec));
351 }
352
353
354
355
356 private class MockRocketClient implements RocketClient {
357 private RocketClient delegate;
358 private boolean isExecuting = false;
359
360 public void setDelegate(RocketClient delegate) {
361 this.delegate = delegate;
362 }
363
364 @Override
365 public <Req, Res> Res execute(RequestSpec<Req, Res> requestSpec) throws RocketRestException {
366
367 if (delegate != null && !isExecuting) {
368 isExecuting = true;
369 try {
370 return delegate.execute(requestSpec);
371 } finally {
372 isExecuting = false;
373 }
374 }
375
376
377 BiFunction<String, Object, Object> responseProducer =
378 findMatchingResponse(requestSpec.getMethod(), requestSpec.getEndpoint());
379
380 if (responseProducer != null) {
381 try {
382 Object response = responseProducer.apply(
383 requestSpec.getEndpoint(),
384 requestSpec.getBody()
385 );
386
387
388 if (requestSpec.getResponseType().isInstance(response)) {
389 @SuppressWarnings("unchecked")
390 Res typedResponse = (Res) response;
391 return typedResponse;
392 } else if (response != null) {
393
394 return objectMapper.convertValue(response, requestSpec.getResponseType());
395 }
396
397 throw new ApiException("Mock response could not be converted to required type: "
398 + requestSpec.getResponseType().getName());
399 } catch (Exception e) {
400 if (e instanceof ApiException) {
401 ApiException apiEx = (ApiException) e;
402 if (apiEx.getStatusCode() > 0) {
403 throw new RocketRestException(
404 apiEx.getMessage(),
405 apiEx.getStatusCode(),
406 apiEx.getResponseBody()
407 );
408 }
409 }
410 throw new RocketRestException("Failed to process mock response", e);
411 }
412 }
413
414 logger.warn("No mock response found for {} : {}", requestSpec.getMethod(), requestSpec.getEndpoint());
415 throw new RocketRestException("No mock response configured for "
416 + requestSpec.getMethod() + ":" + requestSpec.getEndpoint());
417 }
418
419 @Override
420 public void configureSsl(javax.net.ssl.SSLContext sslContext) {
421
422 }
423
424 @Override
425 public void setBaseUrl(String baseUrl) {
426
427 }
428 }
429
430
431
432
433
434
435 private class MockSyncApi implements SyncApi {
436 @Override
437 public <T> T get(String endpoint, Class<T> responseType) {
438 return execute(createGetRequest(endpoint, responseType));
439 }
440
441 @Override
442 public <T> T get(String endpoint, Class<T> responseType, Map<String, String> queryParams) {
443 return execute(createGetRequest(endpoint, responseType, queryParams));
444 }
445
446 @Override
447 public <Res> Res post(String endpoint, Class<Res> responseType) {
448 return execute(createPostRequest(endpoint, responseType));
449 }
450
451 @Override
452 public <Req, Res> Res post(String endpoint, Req body, Class<Res> responseType) {
453 return execute(createPostRequest(endpoint, body, responseType));
454 }
455
456 @Override
457 public <Res> Res put(String endpoint, Class<Res> responseType) {
458 return execute(createPutRequest(endpoint, responseType));
459 }
460
461 @Override
462 public <Req, Res> Res put(String endpoint, Req body, Class<Res> responseType) {
463 return execute(createPutRequest(endpoint, body, responseType));
464 }
465
466 @Override
467 public <T> T delete(String endpoint, Class<T> responseType) {
468 return execute(createDeleteRequest(endpoint, responseType));
469 }
470
471 @Override
472 public <Req, Res> Res execute(RequestSpec<Req, Res> requestSpec) {
473 return RocketRestMock.this.execute(requestSpec);
474 }
475 }
476
477
478
479
480 private class MockAsyncApi implements AsyncApi {
481 @Override
482 public <T> CompletableFuture<T> get(String endpoint, Class<T> responseType) {
483 return executeAsync(createGetRequest(endpoint, responseType));
484 }
485
486 @Override
487 public <T> CompletableFuture<T> get(String endpoint, Class<T> responseType, Map<String, String> queryParams) {
488 return executeAsync(createGetRequest(endpoint, responseType, queryParams));
489 }
490
491 @Override
492 public <Res> CompletableFuture<Res> post(String endpoint, Class<Res> responseType) {
493 return executeAsync(createPostRequest(endpoint, responseType));
494 }
495
496 @Override
497 public <Req, Res> CompletableFuture<Res> post(String endpoint, Req body, Class<Res> responseType) {
498 return executeAsync(createPostRequest(endpoint, body, responseType));
499 }
500
501 @Override
502 public <Res> CompletableFuture<Res> put(String endpoint, Class<Res> responseType) {
503 return executeAsync(createPutRequest(endpoint, responseType));
504 }
505
506 @Override
507 public <Req, Res> CompletableFuture<Res> put(String endpoint, Req body, Class<Res> responseType) {
508 return executeAsync(createPutRequest(endpoint, body, responseType));
509 }
510
511 @Override
512 public <T> CompletableFuture<T> delete(String endpoint, Class<T> responseType) {
513 return executeAsync(createDeleteRequest(endpoint, responseType));
514 }
515
516 @Override
517 public <Req, Res> CompletableFuture<Res> execute(RequestSpec<Req, Res> requestSpec) {
518 return executeAsync(requestSpec);
519 }
520
521 @Override
522 public void shutdown() {
523
524 }
525 }
526
527
528
529
530 private class MockFluentApi implements FluentApi {
531 @Override
532 public <T> Result<T, ApiError> get(String endpoint, Class<T> responseType) {
533 return RocketRestMock.this.executeWithResult(createGetRequest(endpoint, responseType));
534 }
535
536 @Override
537 public <T> Result<T, ApiError> get(String endpoint, Class<T> responseType, Map<String, String> queryParams) {
538 return RocketRestMock.this.executeWithResult(createGetRequest(endpoint, responseType, queryParams));
539 }
540
541 @Override
542 public <Res> Result<Res, ApiError> post(String endpoint, Class<Res> responseType) {
543 return RocketRestMock.this.executeWithResult(createPostRequest(endpoint, responseType));
544 }
545
546 @Override
547 public <Req, Res> Result<Res, ApiError> post(String endpoint, Req body, Class<Res> responseType) {
548 return RocketRestMock.this.executeWithResult(createPostRequest(endpoint, body, responseType));
549 }
550
551 @Override
552 public <Res> Result<Res, ApiError> put(String endpoint, Class<Res> responseType) {
553 return RocketRestMock.this.executeWithResult(createPutRequest(endpoint, responseType));
554 }
555
556 @Override
557 public <Req, Res> Result<Res, ApiError> put(String endpoint, Req body, Class<Res> responseType) {
558 return RocketRestMock.this.executeWithResult(createPutRequest(endpoint, body, responseType));
559 }
560
561 @Override
562 public <T> Result<T, ApiError> delete(String endpoint, Class<T> responseType) {
563 return RocketRestMock.this.executeWithResult(createDeleteRequest(endpoint, responseType));
564 }
565
566 @Override
567 public <Req, Res> Result<Res, ApiError> execute(RequestSpec<Req, Res> requestSpec) {
568 return RocketRestMock.this.executeWithResult(requestSpec);
569 }
570 }
571
572
573 private <T> RequestSpec<Void, T> createGetRequest(String endpoint, Class<T> responseType) {
574 try {
575 java.lang.reflect.Method method = RocketRest.class.getDeclaredMethod("createGetRequest", String.class, Class.class);
576 method.setAccessible(true);
577 @SuppressWarnings("unchecked")
578 RequestSpec<Void, T> result = (RequestSpec<Void, T>) method.invoke(this, endpoint, responseType);
579 return result;
580 } catch (Exception e) {
581 throw new ApiException("Failed to create GET request", e);
582 }
583 }
584
585 private <T> RequestSpec<Void, T> createGetRequest(String endpoint, Class<T> responseType, Map<String, String> queryParams) {
586 try {
587 java.lang.reflect.Method method = RocketRest.class.getDeclaredMethod("createGetRequest", String.class, Class.class, Map.class);
588 method.setAccessible(true);
589 @SuppressWarnings("unchecked")
590 RequestSpec<Void, T> result = (RequestSpec<Void, T>) method.invoke(this, endpoint, responseType, queryParams);
591 return result;
592 } catch (Exception e) {
593 throw new ApiException("Failed to create GET request with params", e);
594 }
595 }
596
597 private <Res> RequestSpec<Void, Res> createPostRequest(String endpoint, Class<Res> responseType) {
598 try {
599 java.lang.reflect.Method method = RocketRest.class.getDeclaredMethod("createPostRequest", String.class, Class.class);
600 method.setAccessible(true);
601 @SuppressWarnings("unchecked")
602 RequestSpec<Void, Res> result = (RequestSpec<Void, Res>) method.invoke(this, endpoint, responseType);
603 return result;
604 } catch (Exception e) {
605 throw new ApiException("Failed to create POST request", e);
606 }
607 }
608
609 private <Req, Res> RequestSpec<Req, Res> createPostRequest(String endpoint, Req body, Class<Res> responseType) {
610 try {
611 java.lang.reflect.Method method = RocketRest.class.getDeclaredMethod("createPostRequest", String.class, Object.class, Class.class);
612 method.setAccessible(true);
613 @SuppressWarnings("unchecked")
614 RequestSpec<Req, Res> result = (RequestSpec<Req, Res>) method.invoke(this, endpoint, body, responseType);
615 return result;
616 } catch (Exception e) {
617 throw new ApiException("Failed to create POST request with body", e);
618 }
619 }
620
621 private <Res> RequestSpec<Void, Res> createPutRequest(String endpoint, Class<Res> responseType) {
622 try {
623 java.lang.reflect.Method method = RocketRest.class.getDeclaredMethod("createPutRequest", String.class, Class.class);
624 method.setAccessible(true);
625 @SuppressWarnings("unchecked")
626 RequestSpec<Void, Res> result = (RequestSpec<Void, Res>) method.invoke(this, endpoint, responseType);
627 return result;
628 } catch (Exception e) {
629 throw new ApiException("Failed to create PUT request", e);
630 }
631 }
632
633 private <Req, Res> RequestSpec<Req, Res> createPutRequest(String endpoint, Req body, Class<Res> responseType) {
634 try {
635 java.lang.reflect.Method method = RocketRest.class.getDeclaredMethod("createPutRequest", String.class, Object.class, Class.class);
636 method.setAccessible(true);
637 @SuppressWarnings("unchecked")
638 RequestSpec<Req, Res> result = (RequestSpec<Req, Res>) method.invoke(this, endpoint, body, responseType);
639 return result;
640 } catch (Exception e) {
641 throw new ApiException("Failed to create PUT request with body", e);
642 }
643 }
644
645 private <T> RequestSpec<Void, T> createDeleteRequest(String endpoint, Class<T> responseType) {
646 try {
647 java.lang.reflect.Method method = RocketRest.class.getDeclaredMethod("createDeleteRequest", String.class, Class.class);
648 method.setAccessible(true);
649 @SuppressWarnings("unchecked")
650 RequestSpec<Void, T> result = (RequestSpec<Void, T>) method.invoke(this, endpoint, responseType);
651 return result;
652 } catch (Exception e) {
653 throw new ApiException("Failed to create DELETE request", e);
654 }
655 }
656 }