My gateway will redirect traffic to many different services (under different domain names). how can i test the gateway's configuration? with only one service i can just setup the mock server (like httpbin) and test the response. with multiple services i'd prefer to avoid starting the whole docker network or changing the locak dns aliases. does spring offer any lightweight way of testing the gateway?
Here is how to achieve what you want with the API Simulator:
package my.package;
import static com.apisimulator.embedded.SuchThat.isEqualTo;
import static com.apisimulator.embedded.SuchThat.startsWith;
import static com.apisimulator.embedded.http.HttpApiSimulation.httpApiSimulation;
import static com.apisimulator.embedded.http.HttpApiSimulation.httpRequest;
import static com.apisimulator.embedded.http.HttpApiSimulation.httpResponse;
import static com.apisimulator.embedded.http.HttpApiSimulation.simlet;
import static com.apisimulator.http.Http1Header.CONTENT_TYPE;
import static com.apisimulator.http.HttpMethod.CONNECT;
import static com.apisimulator.http.HttpMethod.GET;
import static com.apisimulator.http.HttpStatus.OK;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import java.time.Duration;
import java.util.Map;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.SocketUtils;
import com.apisimulator.embedded.http.JUnitHttpApiSimulation;
#RunWith(SpringRunner.class)
#SpringBootTest(
webEnvironment = RANDOM_PORT,
properties = {
"management.server.port=${test.port}", "logging.level.root=info",
// Configure the Gateway to use HTTP proxy - the API Simulator
// instance running at localhost:6090
"spring.cloud.gateway.httpclient.proxy.host=localhost",
"spring.cloud.gateway.httpclient.proxy.port=6090"
//"logging.level.reactor.netty.http.server=debug",
//"spring.cloud.gateway.httpserver.wiretap=true"
}
)
#Import(ServiceGatewayApplication.class)
public class ServiceGatewayApplicationTest
{
// Configure an API simulation. This starts up an instance
// of API Simulator on localhost, default port 6090
#ClassRule
public static final JUnitHttpApiSimulation clApiSimulation = JUnitHttpApiSimulation
.as(httpApiSimulation("svc-gateway-backends"));
protected static int managementPort;
#LocalServerPort
protected int port = 0;
protected String baseUri;
protected WebTestClient webClient;
#BeforeClass
public static void beforeClass()
{
managementPort = SocketUtils.findAvailableTcpPort();
System.setProperty("test.port", String.valueOf(managementPort));
// Configure simlets for the API simulation
// #formatter:off
clApiSimulation.add(simlet("http-proxy")
.when(httpRequest(CONNECT))
.then(httpResponse(OK))
);
clApiSimulation.add(simlet("test-domain-1")
.when(httpRequest()
.whereMethod(GET)
.whereUriPath(isEqualTo("/static"))
// The `host` header is used to determine the actual destination
.whereHeader("host", startsWith("domain-1.com"))
)
.then(httpResponse()
.withStatus(OK)
.withHeader(CONTENT_TYPE, "application/text")
.withBody("{ \"domain\": \"1\" }")
)
);
clApiSimulation.add(simlet("test-domain-2")
.when(httpRequest()
.whereMethod(GET)
.whereUriPath(isEqualTo("/v1/api/foo"))
.whereHeader("host", startsWith("domain-2.com"))
)
.then(httpResponse()
.withStatus(OK)
.withHeader(CONTENT_TYPE, "application/json; charset=UTF-8")
.withBody(
"{\n" +
" \"domain\": \"2\"\n" +
"}"
)
)
);
// #formatter:on
}
#AfterClass
public static void afterClass()
{
System.clearProperty("test.port");
}
#Before
public void setup()
{
// #formatter:off
baseUri = "http://localhost:" + port;
webClient = WebTestClient.bindToServer()
.baseUrl(baseUri)
.responseTimeout(Duration.ofSeconds(2))
.build();
// #formatter:on
}
#Test
public void test_domain1()
{
// #formatter:off
webClient.get()
.uri("/static")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).consumeWith(result ->
assertThat(result.getResponseBody()).isEqualTo("{ \"domain\": \"1\" }")
);
// #formatter:on
}
#Test
public void test_domain2()
{
// #formatter:off
webClient.get()
.uri("/v1/api/foo")
.exchange()
.expectStatus().isOk()
.expectHeader()
.contentType("application/json; charset=UTF-8")
.expectBody(Map.class).consumeWith(result ->
assertThat(result.getResponseBody()).containsEntry("domain", "2")
);
// #formatter:on
}
}
Most of the code is based on this GatewaySampleApplicationTests class from the Spring Cloud Gateway project.
The above assumes the Gateway has routes similar to these (snippets only):
...
uri: "http://domain-1.com"
predicates:
- Path=/static
...
uri: "http://domain-2.com"
predicates:
- Path=/v1/api/foo
...
#apsisim provided a great idea to use web proxy. but the tool he suggests is not in any maven repo and has commercial license. what worked for me:
run the gateway so it will use a proxy (u can be more fancy and find a free port):
private const val proxyPort = 1080
#SpringBootTest(
properties = [
//"logging.level.reactor.netty.http.server=debug",
//"spring.cloud.gateway.httpserver.wiretap=true",
//"spring.cloud.gateway.httpclient.wiretap=true",
"spring.cloud.gateway.httpclient.proxy.host=localhost",
"spring.cloud.gateway.httpclient.proxy.port=$proxyPort"
]
)
then use the mockwebserver as a proxy
testImplementation("com.squareup.okhttp3:mockwebserver:4.2.1")
testImplementation("com.squareup.okhttp3:okhttp:4.2.1")
and then all your requests will go to your proxy. just remember that http protocol specifies that first request to new server requires tunneling via the proxy so when u do first request to the gateway, the gateway will send 2 requests to the proxy:
testClient.get()
.uri(path)
.header("host", gatewayDns)
.exchange()
nextRequestFromGateway {
method `should equal` "CONNECT"
headers[HOST] `should equal` "$realUrlBehindGateway:80"
}
nextRequestFromGateway {
path `should equal` "/api/v1/whatever"
headers[HOST] `should equal` realUrlBehindGateway
}
...
fun nextRequestFromGateway(block : RecordedRequest.() -> Unit) {
mockWebServer.takeRequest().apply (block)
}
Related
I have a spring boot application and I have to connect to some outside service using SSE. WebClient establishes the connection and then I'm using Flux for reading responses. Everything works ok, but the problem is that the connection stays open, because the process is not designed to reach the finish point every time in that 3rd party service. I would like to close the connection manually as a client since I know when this connection should finish. How can I do that?
Establishing connection:
private Flux<ServerSentEvent<String>> connect(String accessToken) {
TcpClient timeoutClient = createTimeoutClient();
ReactorClientHttpConnector reactorClientHttpConnector = new ReactorClientHttpConnector(HttpClient.from(timeoutClient));
String url = npzServerBaseUrl+uniqueCodePath;
WebClient client = WebClient
.builder()
.clientConnector(reactorClientHttpConnector)
.defaultHeader(HttpHeaders.AUTHORIZATION, Naming.TOKEN_PREFIX + accessToken)
.baseUrl(url)
.build();
ParameterizedTypeReference<ServerSentEvent<String>> type
= new ParameterizedTypeReference<ServerSentEvent<String>>() {};
return client.get()
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse -> {
String msg = "Error from server: "+clientResponse.statusCode().toString();
//invalidate access token
if (clientResponse.statusCode().value()==401) {
//remove invalid token and connect again
loginContext.invalidToken(accessToken);
return Mono.error(new InvalidNpzToken(msg));
}
return Mono.error(new IllegalStateException(msg));
}
)
.onStatus(HttpStatus::is5xxServerError, clientResponse ->
Mono.error(new IllegalStateException("Error from server: "+clientResponse.statusCode().toString()))
)
.bodyToFlux(type);
}
private TcpClient createTimeoutClient() {
return TcpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, SECONDS*1000)
.option(EpollChannelOption.TCP_USER_TIMEOUT, SECONDS*1000)
.doOnConnected(
c -> c.addHandlerLast(new ReadTimeoutHandler(SECONDS))
.addHandlerLast(new WriteTimeoutHandler(SECONDS)));
}
Handling content:
Flux<ServerSentEvent<String>> eventStream = connect(accessToken);
eventStream.subscribe(
content -> {
log.info("Time: {} - event: name[{}], id [{}], content[{}] ",
LocalTime.now(), content.event(), content.id(), content.data());
if ("uuid".equals(content.event().trim())) {
listener.receivedUniqueCode(content.data().trim());
} else if ("code".equals(content.event().trim())) {
listener.receivedCode(content.data().trim());
}
},
(Throwable error) -> {
if (error instanceof InvalidToken) {
log.error("Error receiving SSE", error);
//let's retry connection as token has expired
getCode(request, listener);
}
},
() -> log.info("Connection closed!"));
What I expect is that I can call connection.close() or something like that and connection will be closed.
Thanks
Some more information if needed.
Imports:
import io.netty.channel.ChannelOption;
import io.netty.channel.epoll.EpollChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;
import reactor.netty.tcp.TcpClient;
Spring boot:
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
eventStream.subscribe() returns a reactor.core.Disposable
You can call dispose() on it to cancel the subscription and the underlying resources.
I need an endpoint that will work in proxy mode: forward requests to external REST API.
Currently I implemented such class but it's very far away from being ideal.
import java.net.URI;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
#RestController
public class ProxyController2 {
private static final int OFFSET = 4;
private static final String SCHEME = "http";
private static final String HOTS = "127.0.0.1";
private static final String PORT = "9090";
#RequestMapping("/proxy/public/**")
public Mono<String> publicProxy(ServerHttpRequest request) {
HttpMethod httpMethod = request.getMethod();
if (bodyRequired(httpMethod)) {
return WebClient.create()
.method(httpMethod)
.uri(composeTargetUri(request))
.headers(headers -> headers.addAll(request.getHeaders()))
.body(BodyInserters.fromDataBuffers(request.getBody()))
.retrieve()
.bodyToMono(String.class);
} else {
return WebClient.create()
.method(httpMethod)
.uri(composeTargetUri(request))
.headers(headers -> headers.addAll(request.getHeaders()))
.retrieve()
.bodyToMono(String.class);
}
}
private URI composeTargetUri(ServerHttpRequest request) {
return UriComponentsBuilder.newInstance()
.scheme(SCHEME)
.host(HOTS)
.port(PORT)
.path(getTargetPath(request))
.build()
.toUri();
}
private String getTargetPath(ServerHttpRequest request) {
return request.getPath().pathWithinApplication()
.subPath(OFFSET)
.value();
}
private boolean bodyRequired(HttpMethod httpMethod) {
return httpMethod == HttpMethod.DELETE || httpMethod == HttpMethod.POST
|| httpMethod == HttpMethod.PUT;
}
}
It has a few drawbacks:
* It always returns results as string.
* We lose response headers.
* We lose response status (It produces 500 with error message description).
Do you know the good way to create proxy controller in spring webflux application?
Spring Cloud Gateway
An API Gateway built on top of the Spring Ecosystem, including: Spring 5, Spring Boot 2 and Project Reactor. Spring Cloud Gateway aims to provide a simple, yet effective way to route to APIs and provide cross cutting concerns to them such as: security, monitoring/metrics, and resiliency.
Doc: Spring Cloud Gateway
I'm struggling to create a simple stub. I've been following the wiremock tutorial online but have had no success on the issue im facing. Particularly, the "Get" method in the AssertMethod is giving me a lot of issues. I don't know how to resolve it.
public class WeatherApplicationTest {
#Rule
public WireMockRule wireMockRule = new WireMockRule();
public WireMockServer wireMockServer = new WireMockServer(); //No-args constructor will start on port 8080, no HTTPS
#BeforeClass
public void setUpClass() {
wireMockServer.start();
}
#AfterClass
public void tearDownClass() {
wireMockServer.stop();
}
#Test
public void statusMessage() throws IOException{
wireMockRule.stubFor(get(urlEqualTo("/some/thing"))
.willReturn(aResponse()
.withStatus(200)
.withStatusMessage("Everything is fine")
.withHeader("Content-Type", "Text/Plain")));
HttpClient client = new DefaultHttpClient();
HttpGet request = new HttpGet("http://localhost:" + wireMockServer.port() + "/some/thing");
HttpResponse response = client.execute(request);
assertThat(response.GET("/some/thing").statusCode(), is(200));
}
}
I'm getting an error on the following line:
assertThat(response.GET("/some/thing").statusCode(), is(200));
The GET method is underlined in Red.
Please Help!
The WireMock documentation doesn't do a great job of outlining how to connect with it (but, in their defense, it's not really in their slice of the pie).
As I eluded to in the comments, HttpResponse does not contain a GET() method, which is why you're getting the "red underline" (IE: error).
So we know we are looking to make an assertion against the status code. If we look at the Javadoc of the HttpResponse class, there is the StatusLine class that can be retrieved from getStatusLine() of HttpResponse, and the Javadoc of that class shows that it contains a getStatusCode() method. Combining this information into your answer, the assertion needs to be updated to:
assertThat(response.getStatusLine().getStatusCode(), is(200));
In addition, as pointed out by Tom in the comments, remove the WireMockServer (as well as the start/stop and getPort() calls; you can get the port from your rule. And the stubFor(...) method should be called statically (not as a part of the rule).
My contribution to this question: this Java test runs without exceptions, both in the IDE and in Maven as well, and, moreover with the other tests, because at the end, it shuts down the mock server, and doesn't make any conflicts with other servers running in the other tests.
import com.github.tomakehurst.wiremock.WireMockServer;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import wiremock.org.apache.http.HttpResponse;
import wiremock.org.apache.http.client.HttpClient;
import wiremock.org.apache.http.client.methods.HttpGet;
import wiremock.org.apache.http.impl.client.DefaultHttpClient;
import java.io.IOException;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class KatharsisControllerTest {
private static WireMockServer mockedServer;
private static HttpClient client;
#BeforeClass
public static void setUpClass() {
mockedServer = new WireMockServer();
mockedServer.start();
client = new DefaultHttpClient();
}
#AfterClass
public static void tearDown() {
mockedServer.stop();
}
#Test
public void test_get_all_mock() {
stubFor(get(urlEqualTo("/api/Payment"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/vnd.api+json;charset=UTF-8")
.withStatus(200)
.withBody("")
));
HttpGet request = new HttpGet("http://localhost:8080/api/Payment");
try {
HttpResponse response = client.execute(request);
assert response.getStatusLine().getStatusCode() == 200;
} catch (IOException e) {
e.printStackTrace();
}
}
}
Say I have this resource:
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.authz.annotation.RequiresRoles;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
#Path("/authhello")
#Api(value = "hello", description = "Simple endpoints for testing api authentification",
hidden = true)
#Produces(MediaType.APPLICATION_JSON)
#RequiresAuthentication
public class AuthenticatedHelloWorldResource {
private static final String READ = "READ";
private static final String WRITE = "WRITE";
#GET
#ApiOperation(value = "helloworld",
notes = "Simple hello world.",
response = String.class)
#RequiresRoles(READ)
public Response helloWorld() {
String hello = "Hello world!";
return Response.status(Response.Status.OK).entity(hello).build();
}
#GET
#Path("/{param}")
#ApiOperation(value = "helloReply",
notes = "Returns Hello you! and {param}",
response = String.class)
#RequiresRoles(WRITE)
public Response getMsg(#PathParam("param") String msg) {
String output = "Hello you! " + msg;
return Response.status(Response.Status.OK).entity(output).build();
}
}
Should I write tests that confirm that certain (test) users get a response from the endpoints, and certain users don't? And if so: How can I write those tests? I've tried something like this:
import javax.ws.rs.core.Application;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.Test;
import com.cognite.api.shiro.AbstractShiroTest;
import static org.junit.Assert.assertEquals;
public class AuthenticatedHelloWorldTest extends AbstractShiroTest {
#Override
protected Application configure() {
return new ResourceConfig(AuthenticatedHelloWorldResource.class);
}
#Test
public void testAuthenticatedReadHelloWorld() {
final String hello = target("/authhello").request().get(String.class);
assertEquals("Hello world!", hello);
}
#Test
public void testAuthenticatedWriteHelloWorld() {
final String hello = target("/authhello/test").request().get(String.class);
assertEquals("Hello you! test", hello);
}
}
but I'm not sure how to actually test the function of the #RequiresRoles-annotation. I've read Shiro's page on testing, but I haven't been able to write a failing test (e.g. a test for a subject that does not have the WRITE role trying to access /authhello/test). Any tips would be appreciated.
Should I even test this?
Yes. Provided you want to make sure that certain roles will have or have not access to your resource. This will be a security integration test.
How should I go about setting up the whole application + actually call it with an http request in a test if I am to test it? Or is there a simpler way?
Part of the issue is that #RequiresAuthentication and #RequiresRoles themselves are just class and method meta information. Annotations themselves do not provide the security check functionality.
It is not clear from your question what type of container you are using but I can guess that it is plain Jersey JAX-RS service (am I right?). For Shiro to perform security checks you should have added some JAX-RS filter (maybe some other way?) around your endpoints. To test security you should replicate this setup in your tests. Otherwise there is no engine processing your annotations and no security checks as the result.
I have a webservice that I'm trying to invoke with the following client-side code:
import com.test.wsdl.CxfAdd;
import com.test.wsdl.CxfAddService;
import java.util.Map;
import javax.xml.ws.BindingProvider;
import org.apache.cxf.ws.addressing.AddressingProperties;
import org.apache.cxf.ws.addressing.AttributedURIType;
import org.apache.cxf.ws.addressing.EndpointReferenceType;
import org.apache.cxf.ws.addressing.JAXWSAConstants;
public class Main {
public static void main(String[] args) {
CxfAddService service = new CxfAddService();
CxfAdd client = service.getCxfAddPort();
Map<String, Object> requestContext = ((BindingProvider)client).getRequestContext();
AddressingProperties maps = new AddressingProperties();
EndpointReferenceType ref = new EndpointReferenceType();
AttributedURIType add = new AttributedURIType();
add.setValue("http://www.w3.org/2005/08/addressing/anonymous");
ref.setAddress(add);
maps.setReplyTo(ref);
maps.setFaultTo(ref);
requestContext.put(JAXWSAConstants.CLIENT_ADDRESSING_PROPERTIES, maps);
client.myMethodOneWay("Input message");
}
}
On the server-side (Tomcat), the webservice is implemented as the following:
CxfAdd.java:
package com.test.ws;
import javax.jws.Oneway;
import javax.jws.WebResult;
import javax.jws.WebService;
import javax.xml.ws.soap.Addressing;
#WebService(targetNamespace = "http://test.com/wsdl")
#Addressing(enabled = true, required = true)
public interface CxfAdd {
#WebResult(name = "response")
public abstract String myMethod(String message);
#WebResult(name="response")
#Oneway
public void myMethodOneWay(String message);
}
CxfAddImpl.java:
package com.test.ws;
import javax.annotation.Resource;
import javax.jws.Oneway;
import javax.jws.WebService;
import javax.xml.ws.WebServiceContext;
import javax.xml.ws.soap.Addressing;
#WebService
#Addressing
public class CxfAddImpl implements CxfAdd {
#Resource
WebServiceContext webServiceContext;
public String myMethod(String message) {
System.out.println("Invoking sayHello in " + getClass());
return "Hello " + message;
}
#Oneway
public void myMethodOneWay(String message) {
// TODO Auto-generated method stub
}
}
However, when I run the client-side code, at the server-side I get the following error:
INFO: Inbound Message
----------------------------
ID: 46
Address: http://localhost:8080/CxfAddressingServer/services/cxfadd
Encoding: UTF-8
Http-Method: POST
Content-Type: text/xml; charset=UTF-8
Headers: {Accept=[*/*], cache-control=[no-cache], connection=[keep-alive], Content-Length=[211], content-type=[text/xml; charset=UTF-8], host=[localhost:8080], pragma=[no-cache], SOAPAction=[""], user-agent=[Apache CXF 3.1.3]}
Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns2:myMethodOneWay xmlns:ns2="http://test.com/wsdl"><arg0>Input message</arg0></ns2:myMethodOneWay></soap:Body></soap:Envelope>
--------------------------------------
Out 30, 2015 7:18:56 PM org.apache.cxf.ws.addressing.ContextUtils retrieveMAPs
WARNING: WS-Addressing - failed to retrieve Message Addressing Properties from context
It seems that I'm not sending the ws-addressing attributes, can anyone help me figure out what's wrong or missing in my code? Thank you.
In the client-side code, I've replaced the following:
CxfAdd client = service.getCxfAddPort();
With the following:
CxfAdd client = service.getCxfAddPort(
new org.apache.cxf.ws.addressing.WSAddressingFeature());
And it works now.
Solution was provided by someone else here:
http://www.coderanch.com/t/657413/Web-Services/java/CXF-Client-WS-Addressing-attributes