From d0492127175b36eb8780dc4fedbbc24bc5d3a79a Mon Sep 17 00:00:00 2001 From: ThorodanBrom Date: Fri, 25 Mar 2022 18:16:49 +0530 Subject: [PATCH] Testing APD web client with a dummy APD server + bug fix - We create a web server TestApdServerVerticle before testing at port 7331 - Server contains several error conditions and success conditions - Testing for success/failures in GET /userclasses and POST /verify ApdWebClient ------------ - Add constructor so that the port the web client reaches the APD on is configurable - default is 443 - **Bug fix** - handle case if an APD sends an empty response using Optional and throwing a DecodeException if null TestApdServerVerticle --------------------- - Deploys a test server on HTTP at port 7331 - Has error, success cases ApdWebClientTest ---------------- - Deploying ApdWebClient to connect with APDs on port 7331 and not use SSL - Added tests --- .../iudx/aaa/server/apd/ApdWebClient.java | 17 +- .../iudx/aaa/server/apd/ApdWebClientTest.java | 160 +++++++++++ .../aaa/server/apd/TestApdServerVerticle.java | 253 ++++++++++++++++++ 3 files changed, 426 insertions(+), 4 deletions(-) create mode 100644 src/test/java/iudx/aaa/server/apd/ApdWebClientTest.java create mode 100644 src/test/java/iudx/aaa/server/apd/TestApdServerVerticle.java diff --git a/src/main/java/iudx/aaa/server/apd/ApdWebClient.java b/src/main/java/iudx/aaa/server/apd/ApdWebClient.java index fbe94226..51a9f7cd 100644 --- a/src/main/java/iudx/aaa/server/apd/ApdWebClient.java +++ b/src/main/java/iudx/aaa/server/apd/ApdWebClient.java @@ -29,6 +29,7 @@ import iudx.aaa.server.apiserver.Response; import iudx.aaa.server.apiserver.Response.ResponseBuilder; import iudx.aaa.server.apiserver.util.ComposeException; +import java.util.Optional; import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -43,12 +44,19 @@ public class ApdWebClient { private WebClient webClient; private static final Logger LOGGER = LogManager.getLogger(ApdWebClient.class); private static int webClientTimeoutMs; + private static int PORT = 443; public ApdWebClient(WebClient wc, JsonObject options) { this.webClient = wc; webClientTimeoutMs = options.getInteger(CONFIG_WEBCLI_TIMEOUTMS); } + public ApdWebClient(WebClient wc, JsonObject options, int port) { + this.webClient = wc; + webClientTimeoutMs = options.getInteger(CONFIG_WEBCLI_TIMEOUTMS); + PORT = port; + } + static Response failureResponse = new ResponseBuilder().type(URN_INVALID_INPUT) .title(ERR_TITLE_APD_NOT_RESPOND).detail(ERR_DETAIL_APD_NOT_RESPOND).status(400).build(); @@ -65,7 +73,7 @@ public ApdWebClient(WebClient wc, JsonObject options) { public Future checkApdExists(String url) { Promise promise = Promise.promise(); RequestOptions options = new RequestOptions(); - options.setHost(url).setPort(443).setURI(APD_READ_USERCLASSES_API); + options.setHost(url).setPort(PORT).setURI(APD_READ_USERCLASSES_API); webClient.request(HttpMethod.GET, options).timeout(webClientTimeoutMs) .expect(ResponsePredicate.SC_OK).expect(ResponsePredicate.JSON).send().onSuccess(resp -> { @@ -74,7 +82,8 @@ public Future checkApdExists(String url) { * can add further validations if required */ try { - JsonObject json = resp.bodyAsJsonObject(); + JsonObject json = + Optional.ofNullable(resp.bodyAsJsonObject()).orElseThrow(DecodeException::new); promise.complete(true); } catch (DecodeException e) { LOGGER.error("Invalid JSON sent by APD"); @@ -101,7 +110,7 @@ public Future callVerifyApdEndpoint(String url, String authToken, Promise promise = Promise.promise(); RequestOptions options = new RequestOptions(); - options.setHost(url).setPort(443).setURI(APD_VERIFY_API); + options.setHost(url).setPort(PORT).setURI(APD_VERIFY_API); options.addHeader(APD_VERIFY_AUTH_HEADER, APD_VERIFY_BEARER + authToken); webClient.request(HttpMethod.POST, options).timeout(webClientTimeoutMs) @@ -145,7 +154,7 @@ Future checkApdResponse(HttpResponse body) { JsonObject json; try { - json = body.bodyAsJsonObject(); + json = Optional.ofNullable(body.bodyAsJsonObject()).orElseThrow(DecodeException::new); } catch (DecodeException e) { return Future.failedFuture("Invalid JSON sent by APD"); } diff --git a/src/test/java/iudx/aaa/server/apd/ApdWebClientTest.java b/src/test/java/iudx/aaa/server/apd/ApdWebClientTest.java new file mode 100644 index 00000000..bcfa5747 --- /dev/null +++ b/src/test/java/iudx/aaa/server/apd/ApdWebClientTest.java @@ -0,0 +1,160 @@ +package iudx.aaa.server.apd; + +import static iudx.aaa.server.apd.Constants.APD_REQ_PROVIDER; +import static iudx.aaa.server.apd.Constants.APD_REQ_RESOURCE; +import static iudx.aaa.server.apd.Constants.APD_REQ_USER; +import static iudx.aaa.server.apd.Constants.APD_REQ_USERCLASS; +import static iudx.aaa.server.apd.Constants.APD_RESP_DETAIL; +import static iudx.aaa.server.apd.Constants.APD_RESP_SESSIONID; +import static iudx.aaa.server.apd.Constants.APD_RESP_TYPE; +import static iudx.aaa.server.apd.Constants.APD_URN_ALLOW; +import static iudx.aaa.server.apd.Constants.APD_URN_DENY; +import static iudx.aaa.server.apd.Constants.APD_URN_DENY_NEEDS_INT; +import static iudx.aaa.server.apd.Constants.ERR_DETAIL_APD_NOT_RESPOND; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.client.WebClient; +import io.vertx.ext.web.client.WebClientOptions; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; +import iudx.aaa.server.apiserver.util.ComposeException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith({VertxExtension.class}) +@TestMethodOrder(OrderAnnotation.class) +public class ApdWebClientTest { + private static ApdWebClient apdWebClient; + private static TestApdServerVerticle verticle = new TestApdServerVerticle(); + + @BeforeAll + @DisplayName("Deploying Verticle") + static void startVertx(Vertx vertx, VertxTestContext testContext) { + + /* + * Deploying with timeout 4000 instead of picking up from config file. Deploying with no SSL + * checks. Redirects are always not allowed. + */ + JsonObject apdWebCliConfig = new JsonObject().put(Constants.CONFIG_WEBCLI_TIMEOUTMS, 4000); + + WebClientOptions webClientOptions = new WebClientOptions().setSsl(false).setVerifyHost(false) + .setTrustAll(false).setFollowRedirects(false); + WebClient webClient = WebClient.create(vertx, webClientOptions); + + /* TestApdServiceVerticle starts on port 7331, so using this constructor */ + apdWebClient = new ApdWebClient(webClient, apdWebCliConfig, 7331); + + vertx.deployVerticle(verticle, handler -> { + if (handler.succeeded()) { + testContext.completeNow(); + } else { + handler.cause().printStackTrace(); + } + }); + } + + @AfterAll + public static void finish(VertxTestContext testContext) { + verticle.stop(); + testContext.completeNow(); + } + + @Order(1) + @RepeatedTest(TestApdServerVerticle.USERCLASS_ERRORS) + @DisplayName("Test get userclass error cases") + void testGetUserclassErrors(VertxTestContext testContext) { + testContext.assertFailure(apdWebClient.checkApdExists("localhost")).recover(r -> { + assertTrue(r instanceof ComposeException); + assertTrue(r.getLocalizedMessage().equals(ERR_DETAIL_APD_NOT_RESPOND)); + return Future.succeededFuture(); + }).onSuccess(x -> testContext.completeNow()); + } + + @Order(2) + @Test + @DisplayName("Test get userclass success") + void testGetUserclassSuccess(VertxTestContext testContext) { + testContext.assertComplete(apdWebClient.checkApdExists("localhost")).compose(r -> { + assertTrue(r); + return Future.succeededFuture(); + }).onSuccess(x -> testContext.completeNow()); + } + + @Order(3) + @RepeatedTest(TestApdServerVerticle.VERIFY_ERRORS) + @DisplayName("Test post verify error cases") + void testPostVerifyErrors(VertxTestContext testContext) { + /* + * We just put empty objects for user and provider and dummy placeholders for the auth token and + * resource ID + */ + JsonObject request = + new JsonObject().put(APD_REQ_USER, new JsonObject()).put(APD_REQ_PROVIDER, new JsonObject()) + .put(APD_REQ_RESOURCE, "resource").put(APD_REQ_USERCLASS, "TestError"); + testContext.assertFailure(apdWebClient.callVerifyApdEndpoint("localhost", "token", request)) + .recover(r -> { + assertTrue(r instanceof ComposeException); + assertTrue(r.getLocalizedMessage().equals(ERR_DETAIL_APD_NOT_RESPOND)); + return Future.succeededFuture(); + }).onSuccess(x -> testContext.completeNow()); + } + + @Order(4) + @Test + @DisplayName("Test post verify allow") + void testPostVerifyAllow(VertxTestContext testContext) { + JsonObject request = + new JsonObject().put(APD_REQ_USER, new JsonObject()).put(APD_REQ_PROVIDER, new JsonObject()) + .put(APD_REQ_RESOURCE, "resource").put(APD_REQ_USERCLASS, "TestAllow"); + testContext.assertComplete(apdWebClient.callVerifyApdEndpoint("localhost", "token", request)) + .compose(r -> { + assertEquals(r.getString(APD_RESP_TYPE), APD_URN_ALLOW); + return Future.succeededFuture(); + }).onSuccess(x -> testContext.completeNow()); + } + + @Order(5) + @Test + @DisplayName("Test post verify deny") + void testPostVerifyDeny(VertxTestContext testContext) { + JsonObject request = + new JsonObject().put(APD_REQ_USER, new JsonObject()).put(APD_REQ_PROVIDER, new JsonObject()) + .put(APD_REQ_RESOURCE, "resource").put(APD_REQ_USERCLASS, "TestDeny"); + testContext.assertComplete(apdWebClient.callVerifyApdEndpoint("localhost", "token", request)) + .compose(r -> { + assertEquals(r.getString(APD_RESP_TYPE), APD_URN_DENY); + assertTrue(r.containsKey(APD_RESP_DETAIL)); + assertTrue(r.getString(APD_RESP_DETAIL) != null); + return Future.succeededFuture(); + }).onSuccess(x -> testContext.completeNow()); + } + + @Order(6) + @Test + @DisplayName("Test post verify deny-needs-interaction") + void testPostVerifyDenyNeedsInteraction(VertxTestContext testContext) { + JsonObject request = + new JsonObject().put(APD_REQ_USER, new JsonObject()).put(APD_REQ_PROVIDER, new JsonObject()) + .put(APD_REQ_RESOURCE, "resource").put(APD_REQ_USERCLASS, "TestDenyNInteraction"); + testContext.assertComplete(apdWebClient.callVerifyApdEndpoint("localhost", "token", request)) + .compose(r -> { + assertEquals(r.getString(APD_RESP_TYPE), APD_URN_DENY_NEEDS_INT); + assertTrue(r.containsKey(APD_RESP_DETAIL)); + assertTrue(r.containsKey(APD_RESP_SESSIONID)); + assertTrue(r.getString(APD_RESP_DETAIL) != null); + assertTrue(r.getString(APD_RESP_SESSIONID) != null); + return Future.succeededFuture(); + }).onSuccess(x -> testContext.completeNow()); + } +} diff --git a/src/test/java/iudx/aaa/server/apd/TestApdServerVerticle.java b/src/test/java/iudx/aaa/server/apd/TestApdServerVerticle.java new file mode 100644 index 00000000..fa0db5d7 --- /dev/null +++ b/src/test/java/iudx/aaa/server/apd/TestApdServerVerticle.java @@ -0,0 +1,253 @@ +package iudx.aaa.server.apd; + +import static iudx.aaa.server.apd.Constants.APD_READ_USERCLASSES_API; +import static iudx.aaa.server.apd.Constants.APD_REQ_USERCLASS; +import static iudx.aaa.server.apd.Constants.APD_RESP_DETAIL; +import static iudx.aaa.server.apd.Constants.APD_RESP_LINK; +import static iudx.aaa.server.apd.Constants.APD_RESP_SESSIONID; +import static iudx.aaa.server.apd.Constants.APD_RESP_TYPE; +import static iudx.aaa.server.apd.Constants.APD_URN_ALLOW; +import static iudx.aaa.server.apd.Constants.APD_URN_DENY; +import static iudx.aaa.server.apd.Constants.APD_URN_DENY_NEEDS_INT; +import static iudx.aaa.server.apd.Constants.APD_VERIFY_API; + +import io.vertx.core.AbstractVerticle; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.BodyHandler; +import java.util.UUID; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class TestApdServerVerticle extends AbstractVerticle { + + public static final int USERCLASS_ERRORS = 10; + public static final int VERIFY_ERRORS = 21; + private Router router = Router.router(vertx); + private int userclassErrorCounter = 0; + private int verifyErrorCounter = 0; + private HttpServer server; + + private static Logger LOGGER = LogManager.getLogger(TestApdServerVerticle.class); + + @Override + public void start() throws Exception { + + router.get(APD_READ_USERCLASSES_API).handler(context -> { + userclassErrorCounter++; + getUserClass(context, userclassErrorCounter); + }); + + router.post(APD_VERIFY_API).handler(BodyHandler.create()).handler(context -> { + JsonObject body = context.getBodyAsJson(); + if (body.getString(APD_REQ_USERCLASS).equals("TestError")) { + + verifyErrorCounter++; + postVerify(context, verifyErrorCounter); + } else if (body.getString(APD_REQ_USERCLASS).equals("TestDeny")) { + + HttpServerResponse response = context.response(); + JsonObject jsonResponse = + new JsonObject().put(APD_RESP_TYPE, APD_URN_DENY).put(APD_RESP_DETAIL, "Error"); + response.setStatusCode(403).putHeader("Content-type", "application/json") + .end(jsonResponse.encode()); + } else if (body.getString(APD_REQ_USERCLASS).equals("TestDenyNInteraction")) { + + HttpServerResponse response = context.response(); + JsonObject jsonResponse = new JsonObject().put(APD_RESP_TYPE, APD_URN_DENY_NEEDS_INT) + .put(APD_RESP_DETAIL, "Error").put(APD_RESP_SESSIONID, UUID.randomUUID().toString()) + .put(APD_RESP_LINK, "example.com"); + response.setStatusCode(403).putHeader("Content-type", "application/json") + .end(jsonResponse.encode()); + } else { + + HttpServerResponse response = context.response(); + JsonObject jsonResponse = new JsonObject().put(APD_RESP_TYPE, APD_URN_ALLOW); + response.setStatusCode(200).putHeader("Content-type", "application/json") + .end(jsonResponse.encode()); + } + }); + + server = vertx.createHttpServer(); + + server.requestHandler(router).listen(7331).onSuccess(success -> { + LOGGER.debug("Info: Started APD test HTTP server"); + }).onFailure(err -> { + LOGGER.fatal("Info: Failed to start HTTP server - " + err.getMessage()); + }); + } + + @Override + public void stop() { + server.close().onSuccess(success -> { + LOGGER.debug("Info: Stopped APD test HTTP server"); + }).onFailure(err -> { + LOGGER.fatal("Info: Failed to stop HTTP server - " + err.getMessage()); + }); + } + + private void postVerify(RoutingContext context, int counter) { + HttpServerResponse response = context.response(); + JsonObject jsonResponse; + switch (counter) { + case 1: + response.setStatusCode(200).putHeader("Content-type", "application/html") + .end(""); + break; + case 2: + vertx.setTimer(7000, res -> { + response.setStatusCode(200).putHeader("Content-type", "application/json").end("{}"); + }); + break; + case 3: + response.setStatusCode(200).putHeader("Content-type", "application/json") + .end("this is not JSON"); + break; + case 4: + response.setStatusCode(301).putHeader("Location", "example.com").end(); + break; + case 5: + response.setStatusCode(200).putHeader("Content-type", "application/json").end("[]"); + break; + case 6: + response.setStatusCode(200).putHeader("Content-type", "application/json").end(); + break; + case 7: + response.setStatusCode(200).putHeader("Content-type", "application/json").end("{}"); + break; + case 8: + response.setStatusCode(100).putHeader("Content-type", "application/json").end("{}"); + break; + case 9: + response.setStatusCode(200).putHeader("Content-type", + "multipart/form-data; boundary=---------------------------974767299852498929531610575") + .end("{}"); + break; + case 10: + response.setStatusCode(200).putHeader("Content-type", "application/x-www-form-urlencoded") + .end("{}"); + break; + case 11: + /* invalid URN */ + jsonResponse = new JsonObject().put(APD_RESP_TYPE, "urn:apd:random"); + response.setStatusCode(200).putHeader("Content-type", "application/json") + .end(jsonResponse.encode()); + break; + case 12: + /* invalid URN */ + jsonResponse = new JsonObject().put(APD_RESP_TYPE, "urn:apd:random"); + response.setStatusCode(200).putHeader("Content-type", "application/json") + .end(jsonResponse.encode()); + break; + case 13: + /* non 200/403 status code */ + jsonResponse = new JsonObject().put(APD_RESP_TYPE, APD_URN_ALLOW); + response.setStatusCode(401).putHeader("Content-type", "application/json") + .end(jsonResponse.encode()); + break; + case 14: + /* sending 200, but wrong URN */ + jsonResponse = + new JsonObject().put(APD_RESP_TYPE, APD_URN_DENY).put(APD_RESP_DETAIL, "something"); + response.setStatusCode(200).putHeader("Content-type", "application/json") + .end(jsonResponse.encode()); + break; + case 15: + /* sending 403, but wrong URN */ + jsonResponse = new JsonObject().put(APD_RESP_TYPE, APD_URN_ALLOW); + response.setStatusCode(403).putHeader("Content-type", "application/json") + .end(jsonResponse.encode()); + break; + case 16: + /* sending 403+deny, but no detail */ + jsonResponse = new JsonObject().put(APD_RESP_TYPE, APD_URN_DENY); + response.setStatusCode(403).putHeader("Content-type", "application/json") + .end(jsonResponse.encode()); + break; + case 17: + /* sending 403+denyNeedsInteraction, but no detail */ + jsonResponse = new JsonObject().put(APD_RESP_TYPE, APD_URN_DENY_NEEDS_INT); + response.setStatusCode(403).putHeader("Content-type", "application/json") + .end(jsonResponse.encode()); + break; + case 18: + /* sending 403+denyNeedsInteraction, but no sessionId */ + jsonResponse = new JsonObject().put(APD_RESP_TYPE, APD_URN_DENY_NEEDS_INT) + .put(APD_RESP_DETAIL, "something"); + response.setStatusCode(403).putHeader("Content-type", "application/json") + .end(jsonResponse.encode()); + break; + case 19: + /* sending 403+denyNeedsInteraction and sessionId, but no detail */ + jsonResponse = new JsonObject().put(APD_RESP_TYPE, APD_URN_DENY_NEEDS_INT) + .put(APD_RESP_SESSIONID, "something"); + response.setStatusCode(403).putHeader("Content-type", "application/json") + .end(jsonResponse.encode()); + break; + case 20: + /* sending denyNeedsInteraction, sessionId, detail but 200 */ + jsonResponse = new JsonObject().put(APD_RESP_TYPE, APD_URN_DENY_NEEDS_INT) + .put(APD_RESP_SESSIONID, "something").put(APD_RESP_DETAIL, "something"); + response.setStatusCode(200).putHeader("Content-type", "application/json") + .end(jsonResponse.encode()); + break; + case VERIFY_ERRORS: + /* sending nulls in response */ + jsonResponse = new JsonObject().put(APD_RESP_TYPE, null).put(APD_RESP_DETAIL, "something"); + response.setStatusCode(403).putHeader("Content-type", "application/json") + .end(jsonResponse.encode()); + break; + } + return; + } + + private void getUserClass(RoutingContext context, int counter) { + HttpServerResponse response = context.response(); + switch (counter) { + case 1: + response.setStatusCode(200).putHeader("Content-type", "application/html") + .end(""); + break; + case 2: + vertx.setTimer(7000, res -> { + response.setStatusCode(200).putHeader("Content-type", "application/json").end("{}"); + }); + break; + case 3: + response.setStatusCode(200).putHeader("Content-type", "application/json") + .end("this is not JSON"); + break; + case 4: + response.setStatusCode(301).putHeader("Location", "example.com").end(); + break; + case 5: + response.setStatusCode(200).putHeader("Content-type", "application/json").end("[]"); + break; + case 6: + response.setStatusCode(200).putHeader("Content-type", "text/html").end("{}"); + break; + case 7: + response.setStatusCode(200).putHeader("Content-type", + "multipart/form-data; boundary=---------------------------974767299852498929531610575") + .end("{}"); + break; + case 8: + response.setStatusCode(200).putHeader("Content-type", "application/x-www-form-urlencoded") + .end("{}"); + break; + case 9: + response.setStatusCode(200).putHeader("Content-type", "application/json").end(); + break; + case USERCLASS_ERRORS: + response.setStatusCode(400).putHeader("Content-type", "application/json").end("{}"); + break; + default: + response.setStatusCode(200).putHeader("Content-type", "application/json").end("{}"); + } + return; + } + +}