diff --git a/src/main/java/io/cryostat/discovery/Discovery.java b/src/main/java/io/cryostat/discovery/Discovery.java index 508dbd55f..7e7a82fa3 100644 --- a/src/main/java/io/cryostat/discovery/Discovery.java +++ b/src/main/java/io/cryostat/discovery/Discovery.java @@ -39,7 +39,6 @@ import io.cryostat.util.URIUtil; import com.fasterxml.jackson.annotation.JsonView; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.jose.JOSEException; import com.nimbusds.jwt.proc.BadJWTException; @@ -163,15 +162,18 @@ public DiscoveryNode get() { @Path("/api/v4/discovery/{id}") @RolesAllowed("read") public void checkRegistration( - @Context RoutingContext ctx, @RestPath UUID id, @RestQuery String token) - throws SocketException, - UnknownHostException, - MalformedURLException, - ParseException, - JOSEException, - URISyntaxException { + @Context RoutingContext ctx, @RestPath UUID id, @RestQuery String token) { DiscoveryPlugin plugin = DiscoveryPlugin.find("id", id).singleResult(); - jwtValidator.validateJwt(ctx, plugin, token, true); + try { + jwtValidator.validateJwt(ctx, plugin, token, true); + } catch (MalformedURLException + | URISyntaxException + | UnknownHostException + | SocketException + | JOSEException + | ParseException e) { + throw new BadRequestException(e); + } } @Transactional @@ -182,39 +184,54 @@ public void checkRegistration( @RolesAllowed("write") @SuppressFBWarnings("DLS_DEAD_LOCAL_STORE") public PluginRegistration register(@Context RoutingContext ctx, JsonObject body) - throws URISyntaxException, - MalformedURLException, - JOSEException, - UnknownHostException, - SocketException, - ParseException, - BadJWTException, - SchedulerException { + throws SchedulerException { String pluginId = body.getString("id"); String priorToken = body.getString("token"); String realmName = body.getString("realm"); - URI callbackUri = new URI(body.getString("callback")); + URI callbackUri; + try { + String callback = body.getString("callback"); + if (StringUtils.isBlank(callback)) { + throw new BadRequestException("callback cannot be blank"); + } + callbackUri = new URI(callback); + } catch (URISyntaxException e) { + throw new BadRequestException(e); + } URI unauthCallback = UriBuilder.fromUri(callbackUri).userInfo(null).build(); - // URI range validation - if (!uriUtil.validateUri(callbackUri)) { - throw new BadRequestException( - String.format( - "cryostat.target.callback of \"%s\" is unacceptable with the" - + " current URI range settings", - callbackUri)); + InetAddress remoteAddress = getRemoteAddress(ctx); + URI remoteURI; + try { + remoteURI = new URI(remoteAddress.getHostAddress()); + } catch (URISyntaxException e) { + throw new BadRequestException(e); } - InetAddress remoteAddress = getRemoteAddress(ctx); - URI remoteURI = new URI(remoteAddress.getHostAddress()); - if (!uriUtil.validateUri(remoteURI)) { - throw new BadRequestException( - String.format( - "Remote Address of \"%s\" is unacceptable with the" - + " current URI range settings", - remoteURI)); + try { + if (!uriUtil.validateUri(callbackUri)) { + throw new BadRequestException( + String.format( + "cryostat.target.callback of \"%s\" is unacceptable with the" + + " current URI range settings", + callbackUri)); + } + if (!uriUtil.validateUri(remoteURI)) { + throw new BadRequestException( + String.format( + "Remote Address of \"%s\" is unacceptable with the" + + " current URI range settings", + remoteURI)); + } + } catch (MalformedURLException e) { + throw new BadRequestException(e); } + for (var e : new String[] {callbackUri.getScheme(), callbackUri.getHost()}) { + if (StringUtils.isBlank(e)) { + throw new BadRequestException("callback must contain scheme and host"); + } + } if (agentTlsRequired && !callbackUri.getScheme().equals("https")) { throw new BadRequestException( String.format( @@ -233,10 +250,20 @@ public PluginRegistration register(@Context RoutingContext ctx, JsonObject body) throw new ForbiddenException(); } if (!Objects.equals(plugin.callback, unauthCallback)) { - throw new BadRequestException(); + throw new BadRequestException("plugin callback mismatch"); + } + try { + location = jwtFactory.getPluginLocation(plugin); + jwtFactory.parseDiscoveryPluginJwt( + plugin, priorToken, location, remoteAddress, false); + } catch (URISyntaxException + | UnknownHostException + | SocketException + | BadJWTException + | JOSEException + | ParseException e) { + throw new BadRequestException(e); } - location = jwtFactory.getPluginLocation(plugin); - jwtFactory.parseDiscoveryPluginJwt(plugin, priorToken, location, remoteAddress, false); } else { // check if a plugin record with the same callback already exists. If it does, // ping it: @@ -274,7 +301,11 @@ public PluginRegistration register(@Context RoutingContext ctx, JsonObject body) universe.children.add(plugin.realm); universe.persist(); - location = jwtFactory.getPluginLocation(plugin); + try { + location = jwtFactory.getPluginLocation(plugin); + } catch (URISyntaxException e) { + throw new BadRequestException(e); + } var dataMap = new JobDataMap(); dataMap.put(PLUGIN_ID_MAP_KEY, plugin.id); @@ -297,7 +328,12 @@ public PluginRegistration register(@Context RoutingContext ctx, JsonObject body) scheduler.scheduleJob(jobDetail, trigger); } - String token = jwtFactory.createDiscoveryPluginJwt(plugin, remoteAddress, location); + String token; + try { + token = jwtFactory.createDiscoveryPluginJwt(plugin, remoteAddress, location); + } catch (URISyntaxException | JOSEException | UnknownHostException | SocketException e) { + throw new BadRequestException(e); + } // TODO implement more generic env map passing by some platform detection // strategy or generalized config properties @@ -318,26 +354,32 @@ public void publish( @Context RoutingContext ctx, @RestPath UUID id, @RestQuery String token, - List body) - throws SocketException, - UnknownHostException, - MalformedURLException, - ParseException, - JOSEException, - URISyntaxException { + List body) { DiscoveryPlugin plugin = DiscoveryPlugin.find("id", id).singleResult(); - jwtValidator.validateJwt(ctx, plugin, token, true); + try { + jwtValidator.validateJwt(ctx, plugin, token, true); + } catch (MalformedURLException + | URISyntaxException + | UnknownHostException + | SocketException + | JOSEException + | ParseException e) { + throw new BadRequestException(e); + } plugin.realm.children.clear(); plugin.realm.children.addAll(body); for (var b : body) { if (b.target != null) { - // URI range validation - if (!uriUtil.validateUri(b.target.connectUrl)) { - throw new BadRequestException( - String.format( - "Connect URL of \"%s\" is unacceptable with the" - + " current URI range settings", - b.target.connectUrl)); + try { + if (!uriUtil.validateUri(b.target.connectUrl)) { + throw new BadRequestException( + String.format( + "Connect URL of \"%s\" is unacceptable with the" + + " current URI range settings", + b.target.connectUrl)); + } + } catch (MalformedURLException e) { + throw new BadRequestException(e); } if (!uriUtil.isJmxUrl(b.target.connectUrl)) { if (agentTlsRequired && !b.target.connectUrl.getScheme().equals("https")) { @@ -370,15 +412,18 @@ public void publish( @Path("/api/v4/discovery/{id}") @PermitAll public void deregister(@Context RoutingContext ctx, @RestPath UUID id, @RestQuery String token) - throws SocketException, - UnknownHostException, - MalformedURLException, - ParseException, - JOSEException, - URISyntaxException, - SchedulerException { + throws SchedulerException { DiscoveryPlugin plugin = DiscoveryPlugin.find("id", id).singleResult(); - jwtValidator.validateJwt(ctx, plugin, token, false); + try { + jwtValidator.validateJwt(ctx, plugin, token, false); + } catch (MalformedURLException + | URISyntaxException + | UnknownHostException + | SocketException + | JOSEException + | ParseException e) { + throw new BadRequestException(e); + } if (plugin.builtin) { throw new ForbiddenException(); } @@ -396,8 +441,7 @@ public void deregister(@Context RoutingContext ctx, @RestPath UUID id, @RestQuer @JsonView(DiscoveryNode.Views.Flat.class) @Path("/api/v4/discovery_plugins") @RolesAllowed("read") - public List getPlugins(@RestQuery String realm) - throws JsonProcessingException { + public List getPlugins(@RestQuery String realm) { // TODO filter for the matching realm name within the DB query return DiscoveryPlugin.findAll().list().stream() .filter(p -> StringUtils.isBlank(realm) || p.realm.name.equals(realm)) @@ -407,7 +451,7 @@ public List getPlugins(@RestQuery String realm) @GET @Path("/api/v4/discovery_plugins/{id}") @RolesAllowed("read") - public DiscoveryPlugin getPlugin(@RestPath UUID id) throws JsonProcessingException { + public DiscoveryPlugin getPlugin(@RestPath UUID id) { return DiscoveryPlugin.find("id", id).singleResult(); } diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties index 1e5e21dcf..29b072edc 100644 --- a/src/main/resources/application-test.properties +++ b/src/main/resources/application-test.properties @@ -8,6 +8,8 @@ cryostat.discovery.kubernetes.enabled=true quarkus.test.env.JAVA_OPTS_APPEND=-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9091 -Dcom.sun.management.jmxremote.rmi.port=9091 -Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.local.only=false quarkus.http.test-timeout=60s +cryostat.agent.tls.required=false + quarkus.datasource.devservices.enabled=true quarkus.datasource.devservices.image-name=quay.io/cryostat/cryostat-db quarkus.hibernate-orm.log.sql=true diff --git a/src/test/java/io/cryostat/AbstractTestBase.java b/src/test/java/io/cryostat/AbstractTestBase.java new file mode 100644 index 000000000..88e734448 --- /dev/null +++ b/src/test/java/io/cryostat/AbstractTestBase.java @@ -0,0 +1,97 @@ +/* + * Copyright The Cryostat Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cryostat; + +import static io.restassured.RestAssured.given; + +import java.time.Duration; + +import io.cryostat.util.HttpStatusCodeIdentifier; + +import io.restassured.http.ContentType; +import jakarta.inject.Inject; +import org.apache.http.client.utils.URLEncodedUtils; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.logging.Logger; +import org.junit.jupiter.api.BeforeEach; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.HeadBucketRequest; + +public abstract class AbstractTestBase { + + public static final String SELF_JMX_URL = "service:jmx:rmi:///jndi/rmi://localhost:0/jmxrmi"; + public static String SELF_JMX_URL_ENCODED = + URLEncodedUtils.formatSegments(SELF_JMX_URL).substring(1); + public static final String SELFTEST_ALIAS = "selftest"; + + @ConfigProperty(name = "storage.buckets.archives.name") + String archivesBucket; + + @ConfigProperty(name = "test.storage.timeout", defaultValue = "5m") + Duration storageTimeout; + + @ConfigProperty(name = "test.storage.retry", defaultValue = "5s") + Duration storageRetry; + + @Inject Logger logger; + @Inject S3Client storage; + + @BeforeEach + void waitForStorage() throws InterruptedException { + long totalTime = 0; + while (!bucketExists(archivesBucket)) { + long start = System.nanoTime(); + Thread.sleep(storageRetry.toMillis()); + long elapsed = System.nanoTime() - start; + totalTime += elapsed; + if (Duration.ofNanos(totalTime).compareTo(storageTimeout) > 0) { + throw new IllegalStateException("Storage took too long to become ready"); + } + } + } + + private boolean bucketExists(String bucket) { + boolean exists = false; + try { + exists = + HttpStatusCodeIdentifier.isSuccessCode( + storage.headBucket(HeadBucketRequest.builder().bucket(bucket).build()) + .sdkHttpResponse() + .statusCode()); + logger.debugv("Storage bucket \"{0}\" exists? {1}", bucket, exists); + } catch (Exception e) { + logger.warn(e); + } + return exists; + } + + protected int defineSelfCustomTarget() { + return given().basePath("/") + .log() + .all() + .contentType(ContentType.URLENC) + .formParam("connectUrl", SELF_JMX_URL) + .formParam("alias", SELFTEST_ALIAS) + .when() + .post("/api/v4/targets") + .then() + .log() + .all() + .extract() + .jsonPath() + .getInt("id"); + } +} diff --git a/src/test/java/io/cryostat/AbstractTransactionalTestBase.java b/src/test/java/io/cryostat/AbstractTransactionalTestBase.java index 6c6537570..cf17984a7 100644 --- a/src/test/java/io/cryostat/AbstractTransactionalTestBase.java +++ b/src/test/java/io/cryostat/AbstractTransactionalTestBase.java @@ -15,72 +15,15 @@ */ package io.cryostat; -import static io.restassured.RestAssured.given; - -import java.time.Duration; - -import io.cryostat.util.HttpStatusCodeIdentifier; - -import io.restassured.http.ContentType; import jakarta.inject.Inject; -import org.apache.http.client.utils.URLEncodedUtils; -import org.eclipse.microprofile.config.inject.ConfigProperty; import org.flywaydb.core.Flyway; -import org.jboss.logging.Logger; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.model.HeadBucketRequest; - -public abstract class AbstractTransactionalTestBase { - - public static final String SELF_JMX_URL = "service:jmx:rmi:///jndi/rmi://localhost:0/jmxrmi"; - public static String SELF_JMX_URL_ENCODED = - URLEncodedUtils.formatSegments(SELF_JMX_URL).substring(1); - public static final String SELFTEST_ALIAS = "selftest"; - - @ConfigProperty(name = "storage.buckets.archives.name") - String archivesBucket; - - @ConfigProperty(name = "test.storage.timeout", defaultValue = "5m") - Duration storageTimeout; - @ConfigProperty(name = "test.storage.retry", defaultValue = "5s") - Duration storageRetry; +public abstract class AbstractTransactionalTestBase extends AbstractTestBase { - @Inject Logger logger; - @Inject S3Client storage; @Inject Flyway flyway; - @BeforeEach - void waitForStorage() throws InterruptedException { - long totalTime = 0; - while (!bucketExists(archivesBucket)) { - long start = System.nanoTime(); - Thread.sleep(storageRetry.toMillis()); - long elapsed = System.nanoTime() - start; - totalTime += elapsed; - if (Duration.ofNanos(totalTime).compareTo(storageTimeout) > 0) { - throw new IllegalStateException("Storage took too long to become ready"); - } - } - } - - private boolean bucketExists(String bucket) { - boolean exists = false; - try { - exists = - HttpStatusCodeIdentifier.isSuccessCode( - storage.headBucket(HeadBucketRequest.builder().bucket(bucket).build()) - .sdkHttpResponse() - .statusCode()); - logger.debugv("Storage bucket \"{0}\" exists? {1}", bucket, exists); - } catch (Exception e) { - logger.warn(e); - } - return exists; - } - @BeforeEach void migrate() { flyway.migrate(); @@ -91,21 +34,4 @@ void cleanup() { flyway.clean(); flyway.migrate(); } - - protected int defineSelfCustomTarget() { - return given().basePath("/") - .log() - .all() - .contentType(ContentType.URLENC) - .formParam("connectUrl", SELF_JMX_URL) - .formParam("alias", SELFTEST_ALIAS) - .when() - .post("/api/v4/targets") - .then() - .log() - .all() - .extract() - .jsonPath() - .getInt("id"); - } } diff --git a/src/test/java/io/cryostat/discovery/DiscoveryPluginTest.java b/src/test/java/io/cryostat/discovery/DiscoveryPluginTest.java new file mode 100644 index 000000000..d9165782e --- /dev/null +++ b/src/test/java/io/cryostat/discovery/DiscoveryPluginTest.java @@ -0,0 +1,282 @@ +/* + * Copyright The Cryostat Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cryostat.discovery; + +import static io.restassured.RestAssured.given; + +import java.net.URI; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.cryostat.AbstractTransactionalTestBase; +import io.cryostat.targets.Target; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +@QuarkusTest +public class DiscoveryPluginTest extends AbstractTransactionalTestBase { + + @Nested + class Validations { + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"invalid uri", "no.protocol.example.com"}) + void rejectsInvalidCallback(String callback) { + var payload = new HashMap<>(); + payload.put("callback", callback); + payload.put("realm", "test"); + given().log() + .all() + .when() + .body(payload) + .contentType(ContentType.JSON) + .post("/api/v4/discovery") + .then() + .log() + .all() + .and() + .assertThat() + .statusCode(400); + } + + @ParameterizedTest + @NullAndEmptySource + void rejectsInvalidRealmName(String realm) { + var payload = new HashMap<>(); + payload.put("callback", "http://example.com"); + payload.put("realm", realm); + given().log() + .all() + .when() + .body(payload) + .contentType(ContentType.JSON) + .post("/api/v4/discovery") + .then() + .log() + .all() + .and() + .assertThat() + .statusCode(400); + } + + @Test + void rejectsPublishForUnregisteredPlugin() { + given().log() + .all() + .when() + .body(List.of()) + .contentType(ContentType.JSON) + .post("/api/v4/discovery/abcd1234") + .then() + .log() + .all() + .and() + .assertThat() + .statusCode(404); + } + } + + @Test + void workflow() { + // store credentials + var credentialId = + given().log() + .all() + .when() + .formParams( + Map.of( + "username", + "user", + "password", + "pass", + "matchExpression", + "target.connectUrl ==" + + " 'http://localhost:8081/health/liveness'")) + .contentType(ContentType.URLENC) + .post("/api/v4/credentials") + .then() + .log() + .all() + .and() + .assertThat() + .statusCode(201) + .contentType(ContentType.JSON) + .extract() + .jsonPath() + .getLong("id"); + + // register + var realmName = "test_realm"; + var callback = + String.format( + "http://storedcredentials:%d@localhost:8081/health/liveness", credentialId); + var registration = + given().log() + .all() + .when() + .body(Map.of("realm", realmName, "callback", callback)) + .contentType(ContentType.JSON) + .post("/api/v4/discovery") + .then() + .log() + .all() + .and() + .assertThat() + .statusCode(200) + .contentType(ContentType.JSON) + .extract() + .jsonPath(); + var pluginId = registration.getString("id"); + var pluginToken = registration.getString("token"); + MatcherAssert.assertThat(pluginId, Matchers.is(Matchers.not(Matchers.emptyOrNullString()))); + MatcherAssert.assertThat( + pluginToken, Matchers.is(Matchers.not(Matchers.emptyOrNullString()))); + + // test what happens if we publish an update that we have no discoverable targets + given().log() + .all() + .when() + .body(List.of()) + .contentType(ContentType.JSON) + .queryParams(Map.of("token", pluginToken)) + .post(String.format("/api/v4/discovery/%s", pluginId)) + .then() + .log() + .all() + .and() + .assertThat() + .statusCode(204); + + // test what happens if we try to publish an invalid node - in this case, one containing no + // target definition + var node = new Node(null, BaseNodeType.JVM.name(), null); + given().log() + .all() + .when() + .body(List.of(node)) + .contentType(ContentType.JSON) + .queryParams(Map.of("token", pluginToken)) + .post(String.format("/api/v4/discovery/%s", pluginId)) + .then() + .log() + .all() + .and() + .assertThat() + .statusCode(400); + + // test what happens if we publish an acceptable singleton list + var target = new Target(URI.create("http://localhost:8081"), "test-node"); + node = new Node("test-node", BaseNodeType.JVM.name(), target); + given().log() + .all() + .when() + .body(List.of(node)) + .contentType(ContentType.JSON) + .queryParams(Map.of("token", pluginToken)) + .post(String.format("/api/v4/discovery/%s", pluginId)) + .then() + .log() + .all() + .and() + .assertThat() + .statusCode(204); + + // refresh + var refreshedRegistration = + given().log() + .all() + .when() + .body( + Map.of( + "id", + pluginId, + "token", + pluginToken, + "realm", + realmName, + "callback", + callback)) + .contentType(ContentType.JSON) + .post("/api/v4/discovery") + .then() + .log() + .all() + .and() + .assertThat() + .statusCode(200) + .contentType(ContentType.JSON) + .extract() + .jsonPath(); + var refreshedPluginId = refreshedRegistration.getString("id"); + var refreshedPluginToken = refreshedRegistration.getString("token"); + MatcherAssert.assertThat(refreshedPluginId, Matchers.equalTo(pluginId)); + MatcherAssert.assertThat(refreshedPluginToken, Matchers.not(Matchers.equalTo(pluginToken))); + + // deregister + given().log() + .all() + .when() + .queryParams(Map.of("token", pluginToken)) + .delete(String.format("/api/v4/discovery/%s", pluginId)) + .then() + .log() + .all() + .and() + .assertThat() + .statusCode(204); + + // double deregister + given().log() + .all() + .when() + .queryParams(Map.of("token", pluginToken)) + .delete(String.format("/api/v4/discovery/%s", pluginId)) + .then() + .log() + .all() + .and() + .assertThat() + .statusCode(404); + + // publish update when not registered + given().log() + .all() + .when() + .body(List.of(node)) + .contentType(ContentType.JSON) + .queryParams(Map.of("token", pluginToken)) + .post(String.format("/api/v4/discovery/%s", pluginId)) + .then() + .log() + .all() + .and() + .assertThat() + .statusCode(404); + } + + record Node(String name, String nodeType, Target target) {} + + record Target(URI connectUrl, String alias) {} +} diff --git a/src/test/java/io/cryostat/discovery/DiscoveryTest.java b/src/test/java/io/cryostat/discovery/DiscoveryTest.java index fe8b32ed1..726e24d62 100644 --- a/src/test/java/io/cryostat/discovery/DiscoveryTest.java +++ b/src/test/java/io/cryostat/discovery/DiscoveryTest.java @@ -87,7 +87,4 @@ void getDiscoveryPlugins() { Matchers.equalTo("id"), Matchers.not(Matchers.blankOrNullString()))); } } - - // TODO write tests for discovery plugin registration process - } diff --git a/src/test/java/itest/DiscoveryPluginIT.java b/src/test/java/itest/DiscoveryPluginIT.java deleted file mode 100644 index e17b1597f..000000000 --- a/src/test/java/itest/DiscoveryPluginIT.java +++ /dev/null @@ -1,326 +0,0 @@ -/* - * Copyright The Cryostat Authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package itest; - -import java.net.URI; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; - -import io.quarkus.test.junit.QuarkusIntegrationTest; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; -import itest.bases.StandardSelfTest; -import org.hamcrest.MatcherAssert; -import org.hamcrest.Matchers; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestMethodOrder; - -@QuarkusIntegrationTest -@Disabled("TODO") -@TestMethodOrder(OrderAnnotation.class) -class DiscoveryPluginIT extends StandardSelfTest { - - final String realm = getClass().getSimpleName(); - final URI callback = URI.create("http://localhost:8181/"); - private static volatile String id; - private static volatile String token; - - @Test - @Order(1) - void shouldBeAbleToRegister() throws InterruptedException, ExecutionException { - JsonObject body = new JsonObject(Map.of("realm", realm, "callback", callback)); - - CompletableFuture response = new CompletableFuture<>(); - webClient - .post("/api/v4/discovery") - .putHeader("Authorization", "None") - .sendJson( - body, - ar -> { - assertRequestStatus(ar, response); - response.complete(ar.result().bodyAsJsonObject()); - }); - JsonObject resp = response.get(); - JsonObject info = resp.getJsonObject("data").getJsonObject("result"); - DiscoveryPluginIT.id = info.getString("id"); - DiscoveryPluginIT.token = info.getString("token"); - MatcherAssert.assertThat(id, Matchers.not(Matchers.emptyOrNullString())); - MatcherAssert.assertThat(token, Matchers.not(Matchers.emptyOrNullString())); - } - - @Test - @Order(2) - void shouldFailToRegisterWithNonUriCallback() throws InterruptedException, ExecutionException { - JsonObject body = new JsonObject(Map.of("realm", realm, "callback", "not a valid URI")); - - CompletableFuture response = new CompletableFuture<>(); - webClient - .post("/api/v4/discovery") - .putHeader("Authorization", "None") - .sendJson( - body, - ar -> { - response.complete(ar.result().statusCode()); - }); - int code = response.get(); - MatcherAssert.assertThat(code, Matchers.equalTo(400)); - } - - @Test - @Order(3) - void shouldBeAbleToUpdate() throws InterruptedException, ExecutionException { - JsonObject service = new JsonObject(Map.of("connectUrl", callback, "alias", "mynode")); - JsonObject target = - new JsonObject( - Map.of( - "target", - service, - "name", - getClass().getSimpleName(), - "nodeType", - "JVM")); - JsonArray subtree = new JsonArray(List.of(target)); - - CompletableFuture response = new CompletableFuture<>(); - webClient - .post("/api/v4/discovery/" + id) - .addQueryParam("token", token) - .sendJson( - subtree, - ar -> { - assertRequestStatus(ar, response); - response.complete(ar.result().body()); - }); - response.get(); - } - - @Test - @Order(4) - void shouldFailToUpdateWithInvalidSubtreeJson() - throws InterruptedException, ExecutionException { - JsonObject service = new JsonObject(Map.of("connectUrl", callback, "alias", "mynode")); - JsonObject target = - new JsonObject( - Map.of( - "target", - service, - "name", - getClass().getSimpleName(), - "nodeType", - "JVM")); - JsonArray subtree = new JsonArray(List.of(target)); - String body = subtree.encode().replaceAll("\\[", "").replaceAll("\\]", ""); - - CompletableFuture response = new CompletableFuture<>(); - webClient - .post("/api/v4/discovery/" + id) - .addQueryParam("token", token) - .sendBuffer( - Buffer.buffer(body), - ar -> { - response.complete(ar.result().statusCode()); - }); - int code = response.get(); - MatcherAssert.assertThat(code, Matchers.equalTo(400)); - } - - @Test - @Order(5) - void shouldFailToReregisterWithoutToken() throws InterruptedException, ExecutionException { - JsonObject body = new JsonObject(Map.of("realm", realm, "callback", callback)); - - CompletableFuture response = new CompletableFuture<>(); - webClient - .post("/api/v4/discovery") - .putHeader("Authorization", "None") - .sendJson( - body, - ar -> { - response.complete(ar.result().statusCode()); - }); - int code = response.get(); - MatcherAssert.assertThat(code, Matchers.equalTo(400)); - } - - @Test - @Order(6) - void shouldBeAbleToRefreshToken() throws InterruptedException, ExecutionException { - JsonObject body = - new JsonObject( - Map.of("id", id, "realm", realm, "callback", callback, "token", token)); - - CompletableFuture response = new CompletableFuture<>(); - webClient - .post("/api/v4/discovery") - // intentionally don't include this header on refresh - it should still work - // .putHeader("Authorization", "None") - .sendJson( - body, - ar -> { - assertRequestStatus(ar, response); - response.complete(ar.result().bodyAsJsonObject()); - }); - JsonObject resp = response.get(); - JsonObject info = resp.getJsonObject("data").getJsonObject("result"); - String newId = info.getString("id"); - MatcherAssert.assertThat(newId, Matchers.equalTo(DiscoveryPluginIT.id)); - MatcherAssert.assertThat(newId, Matchers.not(Matchers.emptyOrNullString())); - DiscoveryPluginIT.id = newId; - - String newToken = info.getString("token"); - MatcherAssert.assertThat(newToken, Matchers.not(Matchers.equalTo(DiscoveryPluginIT.token))); - MatcherAssert.assertThat(token, Matchers.not(Matchers.emptyOrNullString())); - DiscoveryPluginIT.token = newToken; - } - - @Test - @Order(7) - void shouldBeAbleToDeregister() throws InterruptedException, ExecutionException { - CompletableFuture response = new CompletableFuture<>(); - webClient - .delete("/api/v4/discovery/" + id) - .addQueryParam("token", token) - .send( - ar -> { - assertRequestStatus(ar, response); - response.complete(ar.result().statusCode()); - }); - int code = response.get(); - MatcherAssert.assertThat(code, Matchers.equalTo(200)); - } - - @Test - @Order(8) - void shouldFailToDoubleDeregister() throws InterruptedException, ExecutionException { - CompletableFuture response = new CompletableFuture<>(); - webClient - .delete("/api/v4/discovery/" + id) - .addQueryParam("token", token) - .send( - ar -> { - response.complete(ar.result().statusCode()); - }); - int code = response.get(); - MatcherAssert.assertThat(code, Matchers.equalTo(404)); - } - - @Test - @Order(9) - void shouldFailToUpdateUnregisteredPluginID() throws InterruptedException, ExecutionException { - JsonObject service = new JsonObject(Map.of("connectUrl", callback, "alias", "mynode")); - JsonObject target = - new JsonObject( - Map.of( - "target", - service, - "name", - getClass().getSimpleName(), - "nodeType", - "JVM")); - JsonArray subtree = new JsonArray(List.of(target)); - - CompletableFuture response = new CompletableFuture<>(); - webClient - .post("/api/v4/discovery/" + UUID.randomUUID()) - .addQueryParam("token", token) - .sendJson( - subtree, - ar -> { - response.complete(ar.result().statusCode()); - }); - int code = response.get(); - MatcherAssert.assertThat(code, Matchers.equalTo(404)); - } - - @Test - @Order(10) - void shouldFailToRegisterNullCallback() throws InterruptedException, ExecutionException { - JsonObject body = new JsonObject(Map.of("realm", realm)); - - CompletableFuture response = new CompletableFuture<>(); - webClient - .post("/api/v4/discovery") - .putHeader("Authorization", "None") - .sendJson( - body, - ar -> { - response.complete(ar.result().statusCode()); - }); - int code = response.get(); - MatcherAssert.assertThat(code, Matchers.equalTo(400)); - } - - @Test - @Order(11) - void shouldFailToRegisterEmptyCallback() throws InterruptedException, ExecutionException { - JsonObject body = new JsonObject(Map.of("realm", realm, "callback", "")); - - CompletableFuture response = new CompletableFuture<>(); - webClient - .post("/api/v4/discovery") - .putHeader("Authorization", "None") - .sendJson( - body, - ar -> { - response.complete(ar.result().statusCode()); - }); - int code = response.get(); - MatcherAssert.assertThat(code, Matchers.equalTo(400)); - } - - @Test - @Order(12) - void shouldFailToRegisterNullRealm() throws InterruptedException, ExecutionException { - JsonObject body = new JsonObject(Map.of("callback", callback)); - - CompletableFuture response = new CompletableFuture<>(); - webClient - .post("/api/v4/discovery") - .putHeader("Authorization", "None") - .sendJson( - body, - ar -> { - response.complete(ar.result().statusCode()); - }); - int code = response.get(); - MatcherAssert.assertThat(code, Matchers.equalTo(400)); - } - - @Test - @Order(13) - void shouldFailToRegisterEmptyRealm() throws InterruptedException, ExecutionException { - JsonObject body = new JsonObject(Map.of("realm", "", "callback", callback)); - - CompletableFuture response = new CompletableFuture<>(); - webClient - .post("/api/v4/discovery") - .putHeader("Authorization", "None") - .sendJson( - body, - ar -> { - response.complete(ar.result().statusCode()); - }); - int code = response.get(); - MatcherAssert.assertThat(code, Matchers.equalTo(400)); - } -}