Skip to content

Commit

Permalink
Enhance ShadowingInterceptor (#1260)
Browse files Browse the repository at this point in the history
* Enhance ShadowingInterceptor by adding exchange construction method

Refactor cloneRequestAndSend to accept an Exchange parameter and use a new buildExchange method for creating requests. Update tests to validate the new functionality in ShadowingInterceptor.

* Enhance HeaderField and ShadowingInterceptor to use new HeaderName constructor. Update tests to validate header behavior.

* Refactor imports in ShadowingInterceptorTest to streamline code structure

* Refactor ShadowingInterceptorTest to use Mockito for ReturnInterceptor

Update the test class by removing unnecessary imports and extending the base class. Implement Mockito to create a spy for ReturnInterceptor to verify its behavior during the interceptor test. Ensure proper setup and teardown of routers and interceptors for better isolation in tests.

* Refactor ShadowingInterceptor tests for clarity and consistency

- Rename router variables to improve clarity: `router` to `interceptorRouter`, `router2` to `shadowingRouter`.
- Update corresponding rule variable names to match new router names: `shadowingRule` to `interceptorRule`.
- Enhance test method name to `testIfShadowTargetIsCalled()` for better description.
- Clean up initialization and shutdown logic in test setup and teardown.

* Refactor ShadowingInterceptorTest to use RestAssured for HTTP calls

Replace custom assertion method getAndAssert200 with a RestAssured equivalent for improved readability and consistency in test case.

* Refactor HeaderField and HeaderName constructors for efficiency

Remove unnecessary object creation in HeaderField copy constructor. Simplify HeaderName by removing redundant copy constructor.

* Enhance ShadowingInterceptor to include header in request cloning

- Add header parameter to cloneRequestAndSend and buildExchange methods
- Capture request headers for shadowing functionality
- Update tests to verify header handling during shadow requests

* Enhance ShadowingInterceptor to copy request headers for cloned requests. Update method parameters for clarity and consistency. Improve error logging to reflect shadow target details.
  • Loading branch information
christiangoerdes authored Sep 6, 2024
1 parent 46d078d commit db40fd1
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public HeaderField(HeaderField element) {
headerName = element.headerName;
value = element.value;
}

public String getValue() {
return value;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
import com.predic8.membrane.annot.MCChildElement;
import com.predic8.membrane.annot.MCElement;
import com.predic8.membrane.core.exchange.Exchange;
import com.predic8.membrane.core.http.AbstractBody;
import com.predic8.membrane.core.http.Chunk;
import com.predic8.membrane.core.http.MessageObserver;
import com.predic8.membrane.core.http.Request;
import com.predic8.membrane.core.http.*;
import com.predic8.membrane.core.interceptor.AbstractInterceptor;
import com.predic8.membrane.core.interceptor.Outcome;
import com.predic8.membrane.core.rules.AbstractServiceProxy.Target;
import com.predic8.membrane.core.transport.http.HttpClient;
import com.predic8.membrane.core.util.URIFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
Expand All @@ -31,6 +31,8 @@ public class ShadowingInterceptor extends AbstractInterceptor {

@Override
public Outcome handleRequest(Exchange exc) throws Exception {
// Copy the request headers to ensure we maintain original request details for use in the cloned requests.
Header copiedHeader = new Header(exc.getRequest().getHeader());
exc.getRequest().getBody().getObservers().add(new MessageObserver() {
@Override
public void bodyRequested(AbstractBody body) {}
Expand All @@ -40,8 +42,8 @@ public void bodyChunk(Chunk chunk) {}
public void bodyChunk(byte[] buffer, int offset, int length) {}

@Override
public void bodyComplete(AbstractBody body) {
cloneRequestAndSend(body);
public void bodyComplete(AbstractBody completeBody) {
cloneRequestAndSend(completeBody, exc, copiedHeader);
}
});
return CONTINUE;
Expand All @@ -52,39 +54,52 @@ public String getShortDescription() {
return "Sends requests to shadow hosts (processed in the background).";
}

public void cloneRequestAndSend(AbstractBody body) {
public void cloneRequestAndSend(AbstractBody completeBody, Exchange mainExchange, Header copiedHeader) {
ExecutorService executor = newCachedThreadPool();
for (Target target : targets) {
Exchange exc;
for (Target shadowTarget : targets) {
Exchange newExchange;
try {
exc = new Request.Builder()
.body(body.getContent())
.get(getDestFromTarget(target, router.getParentProxy(this).getKey().getPath()))
.buildExchange();
newExchange = buildExchange(completeBody, mainExchange, shadowTarget, copiedHeader);
} catch (Exception e) {
log.error("Error creating request for target {}", target, e);
log.error("Error creating request for target {}", shadowTarget, e);
continue;
}

executor.submit(() -> {
try {
Exchange res = performCall(exc);
Exchange res = performCall(newExchange);
if (res.getResponse().getStatusCode() >= 500)
log.info("{} returned StatusCode {}", res.getDestinations().get(0), res.getResponse().getStatusCode());
} catch (Exception e) {
log.error("Error performing call for target {}", target, e);
log.error("Error performing call for target {}", shadowTarget, e);
}
});
}
}

static Exchange buildExchange(AbstractBody completeBody, Exchange mainExchange, Target shadowTarget, Header copiedHeader) throws URISyntaxException, IOException {
// Build the new Exchange object with the same body, method, and header but targeted at the shadow host.
return new Request.Builder()
.body(completeBody.getContent())
.header(copiedHeader)
.method(mainExchange.getRequest().getMethod())
.url(
new URIFactory(),
getDestFromTarget(
shadowTarget,
mainExchange.getOriginalRequestUri()
)
)
.buildExchange();
}


static String getDestFromTarget(Target t, String path) {
return (t.getUrl() != null) ? t.getUrl() : extracted(t, path);
return (t.getUrl() != null) ? t.getUrl() : buildTargetUrl(t, path);
}

@SuppressWarnings("HttpUrlsUsage")
private static String extracted(Target t, String path) {
private static String buildTargetUrl(Target t, String path) {
return ((t.getSslParser() != null) ? "https://" : "http://") +
t.getHost() +
":" +
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,144 @@
package com.predic8.membrane.core.interceptor.shadowing;

import com.predic8.membrane.core.Router;
import com.predic8.membrane.core.exchange.Exchange;
import com.predic8.membrane.core.exchangestore.ForgetfulExchangeStore;
import com.predic8.membrane.core.http.Body;
import com.predic8.membrane.core.http.Header;
import com.predic8.membrane.core.http.Request;
import com.predic8.membrane.core.interceptor.misc.ReturnInterceptor;
import com.predic8.membrane.core.interceptor.misc.SetHeaderInterceptor;
import com.predic8.membrane.core.rules.AbstractServiceProxy.Target;
import com.predic8.membrane.core.rules.Rule;
import com.predic8.membrane.core.rules.ServiceProxy;
import com.predic8.membrane.core.rules.ServiceProxyKey;
import com.predic8.membrane.core.transport.http.HttpTransport;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
import java.util.List;

import static com.predic8.membrane.core.http.MimeType.APPLICATION_JSON;
import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

class ShadowingInterceptorTest {

@Mock
private Target mockTarget;
Exchange exc;
Header header;

static Router interceptorRouter;
static Router shadowingRouter;

static Rule interceptorRule;
static ShadowingInterceptor shadowingInterceptor;

static ReturnInterceptor returnInterceptorMock;

static Rule shadowingRule;

@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
void setUp() throws Exception {
header = new Header(){{
add(CONTENT_TYPE, APPLICATION_JSON);
}};
exc = ShadowingInterceptor.buildExchange(
new Body("foo".getBytes()),
new Request.Builder()
.post("https://www.google.com")
.header(header)
.buildExchange(),
new Target() {{
setUrl("https://www.predic8.com:9000/foo");
}},
header
);
}

@BeforeAll
static void startup() throws Exception {
interceptorRouter = new Router();
interceptorRouter.setHotDeploy(false);
interceptorRouter.setExchangeStore(new ForgetfulExchangeStore());
interceptorRouter.setTransport(new HttpTransport());

interceptorRule = new ServiceProxy(new ServiceProxyKey("localhost", "*", ".*", 2000), null, 0);
shadowingInterceptor = new ShadowingInterceptor();
shadowingInterceptor.setTargets(List.of(new Target() {{
setHost("localhost");
setPort(3000);
}}));
interceptorRule.setInterceptors(List.of(
shadowingInterceptor,
new SetHeaderInterceptor() {{
setName("foo");
setValue("bar");
}},
new ReturnInterceptor()
));

interceptorRouter.getRuleManager().addProxyAndOpenPortIfNew(interceptorRule);
interceptorRouter.init();
interceptorRouter.start();

shadowingRouter = new Router();
shadowingRouter.setHotDeploy(false);
shadowingRouter.setExchangeStore(new ForgetfulExchangeStore());
shadowingRouter.setTransport(new HttpTransport());

shadowingRule = new ServiceProxy(new ServiceProxyKey("localhost", "*", ".*", 3000), null, 0);
returnInterceptorMock = Mockito.spy(new ReturnInterceptor());
returnInterceptorMock.setStatusCode(200);
shadowingRule.setInterceptors(List.of(returnInterceptorMock));

shadowingRouter.getRuleManager().addProxyAndOpenPortIfNew(shadowingRule);
shadowingRouter.init();
shadowingRouter.start();
}

@AfterAll
static void shutdown() {
shadowingRouter.stop();
interceptorRouter.stop();
}

/**
* Verifies that the shadow target is called by sending a request through the router
* and ensures that the ReturnInterceptor's handleRequest() is invoked once.
*/
@Test
void testGetDestFromTarget_WithUrl() {
when(mockTarget.getUrl()).thenReturn("http://example.com");
String result = ShadowingInterceptor.getDestFromTarget(mockTarget, "/path");
assertEquals("http://example.com", result);
void testIfShadowTargetIsCalled() throws Exception {
given().when().get("http://localhost:2000").then().statusCode(200);
verify(returnInterceptorMock, times(1)).handleRequest(any(Exchange.class));
}

/**
* Verifies that the shadow target is called and the ReturnInterceptor's
* handleRequest() is invoked with an Exchange object not containing the "foo" header.
*/
@Test
void testGetDestFromTarget_WithoutUrl() {
when(mockTarget.getHost()).thenReturn("localhost");
when(mockTarget.getPort()).thenReturn(8080);
String result = ShadowingInterceptor.getDestFromTarget(mockTarget, "/path");
assertEquals("http://localhost:8080/path", result);
void testIfShadowTargetHasFooHeader() throws Exception {
given().when().get("http://localhost:2000").then().statusCode(200);

ArgumentCaptor<Exchange> exchangeCaptor = ArgumentCaptor.forClass(Exchange.class);
verify(returnInterceptorMock, atLeastOnce()).handleRequest(exchangeCaptor.capture());

assertNull(exchangeCaptor.getValue().getRequest().getHeader().getFirstValue("foo"));
}

}

@Test
void buildExchangeTest() {
assertNotNull(exc);
assertEquals("POST", exc.getRequest().getMethod());
assertEquals("/foo", exc.getRequest().getUri());
assertEquals("https://www.predic8.com:9000/foo", exc.getDestinations().get(0));
assertEquals(APPLICATION_JSON, exc.getRequest().getHeader().getContentType());
}
}

0 comments on commit db40fd1

Please sign in to comment.