From e9979230e32b568e049348fea11d29b823b59b18 Mon Sep 17 00:00:00 2001 From: Kai Hudalla Date: Tue, 26 May 2020 13:34:05 +0200 Subject: [PATCH] Remove obsolete get method The Device Registration API does not define a get operation anymore. Thus, the corresponding get method has been removed from the DeviceRegistration interface in the client module. The Command & Control functionality in the Kerlink Lora provider has been removed because it erroneously relied on this method. Signed-off-by: Kai Hudalla --- adapters/lora-vertx/pom.xml | 13 - .../lora/impl/LoraProtocolAdapter.java | 413 +-------------- .../lora/providers/KerlinkProvider.java | 343 +------------ .../lora/providers/KerlinkProviderTest.java | 474 +----------------- .../adapter/lora/providers/LoraTestUtil.java | 36 +- .../hono/client/RegistrationClient.java | 45 +- .../client/impl/RegistrationClientImpl.java | 41 +- site/homepage/content/release-notes.md | 9 + .../tests/jms/JmsBasedRegistrationClient.java | 8 - 9 files changed, 39 insertions(+), 1343 deletions(-) diff --git a/adapters/lora-vertx/pom.xml b/adapters/lora-vertx/pom.xml index d896a42a97..26b765934b 100644 --- a/adapters/lora-vertx/pom.xml +++ b/adapters/lora-vertx/pom.xml @@ -38,24 +38,11 @@ io.vertx vertx-core - - io.vertx - vertx-web-client - com.fasterxml.jackson.core jackson-annotations ${jackson.version} - - com.google.guava - guava - - - com.github.tomakehurst - wiremock - test - org.hamcrest hamcrest-core diff --git a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/impl/LoraProtocolAdapter.java b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/impl/LoraProtocolAdapter.java index 1c515ec22a..9ad7c58987 100644 --- a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/impl/LoraProtocolAdapter.java +++ b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/impl/LoraProtocolAdapter.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019 Contributors to the Eclipse Foundation + * Copyright (c) 2019, 2020 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -13,16 +13,12 @@ package org.eclipse.hono.adapter.lora.impl; -import java.net.HttpURLConnection; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.concurrent.atomic.AtomicBoolean; -import org.apache.qpid.proton.amqp.transport.ErrorCondition; import org.apache.qpid.proton.message.Message; import org.eclipse.hono.adapter.http.AbstractVertxBasedHttpProtocolAdapter; import org.eclipse.hono.adapter.lora.LoraConstants; @@ -30,13 +26,7 @@ import org.eclipse.hono.adapter.lora.LoraProtocolAdapterProperties; import org.eclipse.hono.adapter.lora.providers.LoraProvider; import org.eclipse.hono.adapter.lora.providers.LoraProviderMalformedPayloadException; -import org.eclipse.hono.adapter.lora.providers.LoraUtils; import org.eclipse.hono.auth.Device; -import org.eclipse.hono.client.Command; -import org.eclipse.hono.client.CommandContext; -import org.eclipse.hono.client.CommandResponse; -import org.eclipse.hono.client.HonoConnection; -import org.eclipse.hono.client.MessageConsumer; import org.eclipse.hono.service.auth.device.HonoClientBasedAuthProvider; import org.eclipse.hono.service.auth.device.SubjectDnCredentials; import org.eclipse.hono.service.auth.device.TenantServiceBasedX509Authentication; @@ -49,30 +39,21 @@ import org.eclipse.hono.service.http.X509AuthHandler; import org.eclipse.hono.tracing.TracingHelper; import org.eclipse.hono.util.Constants; -import org.eclipse.hono.util.CredentialsObject; import org.eclipse.hono.util.MessageHelper; -import org.eclipse.hono.util.RegistrationConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import io.opentracing.Span; -import io.opentracing.SpanContext; import io.opentracing.contrib.vertx.ext.web.TracingHandler; import io.opentracing.noop.NoopSpan; import io.opentracing.tag.Tags; -import io.vertx.core.Future; -import io.vertx.core.MultiMap; import io.vertx.core.buffer.Buffer; import io.vertx.core.buffer.impl.BufferImpl; import io.vertx.core.http.HttpMethod; -import io.vertx.core.http.HttpVersion; -import io.vertx.core.json.DecodeException; -import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.client.HttpResponse; import io.vertx.ext.web.handler.ChainAuthHandler; @@ -83,18 +64,15 @@ public final class LoraProtocolAdapter extends AbstractVertxBasedHttpProtocolAda private static final String ERROR_MSG_MISSING_OR_UNSUPPORTED_CONTENT_TYPE = "missing or unsupported content-type"; private static final Logger LOG = LoggerFactory.getLogger(LoraProtocolAdapter.class); - private static final String LORA_COMMAND_CONSUMER_DEVICE_ID = "lora"; - private static final int LORA_COMMAND_CONSUMER_RETRY_INTERVAL = 2_000; + private static final String ERROR_MSG_INVALID_PAYLOAD = "invalid payload"; + private static final String ERROR_MSG_JSON_MISSING_REQUIRED_FIELDS = "JSON Body does not contain required fields"; private static final String TAG_LORA_DEVICE_ID = "lora_device_id"; private static final String TAG_LORA_PROVIDER = "lora_provider"; - private static final String JSON_MISSING_REQUIRED_FIELDS = "JSON Body does not contain required fields"; - private static final String INVALID_PAYLOAD = "Invalid payload"; private final List loraProviders = new ArrayList<>(); private HonoClientBasedAuthProvider usernamePasswordAuthProvider; private HonoClientBasedAuthProvider clientCertAuthProvider; - private final AtomicBoolean startOfLoraCommandConsumersScheduled = new AtomicBoolean(); /** * Sets the LoRa providers that this adapter should support. @@ -235,7 +213,7 @@ void handleProviderRoute(final RoutingContext ctx, final LoraProvider provider) LOG.debug("Got invalid payload '{}' which leads to exception: {}", loraMessage, e); TracingHelper.logError(currentSpan, "Received message of type '" + type + "' has invalid payload; error: " + e); - handle400(ctx, INVALID_PAYLOAD); + handle400(ctx, ERROR_MSG_INVALID_PAYLOAD); } } else { LOG.debug("Supplied credentials are not an instance of the user. Returning 401"); @@ -277,7 +255,7 @@ private void doUpload(final RoutingContext ctx, final Device device, final Strin if (payload == null) { TracingHelper.logError(getCurrentSpan(ctx), "Got message without valid payload"); } - handle400(ctx, JSON_MISSING_REQUIRED_FIELDS); + handle400(ctx, ERROR_MSG_JSON_MISSING_REQUIRED_FIELDS); } } @@ -302,385 +280,4 @@ private void handle200(final RoutingContext ctx) { ctx.response().setStatusCode(200); ctx.response().end(); } - - @Override - protected void onCommandConnectionEstablished(final HonoConnection commandConnection) { - if (startOfLoraCommandConsumersScheduled.compareAndSet(false, true)) { - scheduleStartLoraCommandConsumers(); - } - } - - private void scheduleStartLoraCommandConsumers() { - final List commandEnabledTenants = getConfig().getCommandEnabledTenants(); - LOG.info("Starting Lora command consumers for tenants [{}] ...", commandEnabledTenants); - for (final String tenantId : commandEnabledTenants) { - scheduleStartLoraCommandConsumer(tenantId); - } - } - - private void scheduleStartLoraCommandConsumer(final String tenantId) { - LOG.info("Starting Lora command consumer for tenant '{}' ...", tenantId); - startLoraCommandConsumer(tenantId).recover(x -> { - LOG.error("Error starting initial Lora command consumer for tenant [{}], retry in {} ms", tenantId, - LORA_COMMAND_CONSUMER_RETRY_INTERVAL, x); - vertx.setTimer(LORA_COMMAND_CONSUMER_RETRY_INTERVAL, y -> scheduleStartLoraCommandConsumer(tenantId)); - return Future.succeededFuture(); - }); - } - - private Future startLoraCommandConsumer(final String tenantId) { - - return getCommandConsumerFactory().createCommandConsumer( - tenantId, - LORA_COMMAND_CONSUMER_DEVICE_ID, - receivedCommandContext -> commandConsumer(tenantId, receivedCommandContext), - remoteClose -> { - LOG.info("Closing command consumer"); - }, - LORA_COMMAND_CONSUMER_RETRY_INTERVAL); - } - - private void commandConsumer(final String tenantId, final CommandContext receivedCommandContext) { - Tags.COMPONENT.set(receivedCommandContext.getCurrentSpan(), getTypeName()); - final Command command = receivedCommandContext.getCommand(); - final CommandData commandData = new CommandData(); - commandData.setCommand(command); - - getTenantConfiguration(tenantId, receivedCommandContext.getCurrentSpan().context()).compose(tenantObject -> { - if (tenantObject.isAdapterEnabled(getTypeName())) { - final String logMsg = "Adapter " + getTypeName() + " is enabled for tenant " + tenantId; - LOG.debug(logMsg); - return Future.succeededFuture(); - } else { - final String logMsg = "Adapter " + getTypeName() + " is not enabled for tenant " + tenantId; - LOG.error(logMsg); - return Future.failedFuture(logMsg); - } - }).compose(adapterEnabledForTenant -> { - // we accept the message before sending it to the device in order to provide a consistent behavior - // across all protocol adapters, i.e. accepting the message only means that the message contains - // a valid command which we are willing to deliver see - // org.eclipse.hono.adapter.http.AbstractVertxBasedHttpProtocolAdapter.createCommandConsumer - receivedCommandContext.accept(1); - - if (isValidLoraCommand(command)) { - - final String loraDeviceId = (String) command.getApplicationProperties() - .get(MessageHelper.APP_PROPERTY_DEVICE_ID); - receivedCommandContext.getCurrentSpan().setTag(TAG_LORA_DEVICE_ID, loraDeviceId); - commandData.setTargetDeviceId(loraDeviceId); - LOG.debug("Got valid command {} for an actual lora device [{}]", command, loraDeviceId); - - final Future loraGatewayFuture = getLoraDeviceGateway(tenantId, loraDeviceId); - - loraGatewayFuture - .compose(loraGateway -> getRegistrationAssertion(tenantId, loraDeviceId, - new Device(tenantId, loraGateway.getString("device-id")), - receivedCommandContext.getCurrentSpan().context()) - .compose(registrationAssertion -> { - LOG.debug("Lora device {} registered and enabled for the Lora gateway.", - loraDeviceId); - commandData.setGatewayAndExtractLoraNetworkData(loraGatewayFuture.result()) - .compose(loraNetworkData -> getGatewayCredentials(tenantId, - loraNetworkData)) - .compose(loraGatewayCredentials -> { - LOG.debug( - "Successfully received gateway credentials for lora device " - + "'{}'", - commandData.getTargetDeviceId()); - commandData.setGatewayCredentials(loraGatewayCredentials); - return sendCommandToDevice(commandData); - }).compose(httpResponse -> { - LOG.debug( - "Received status code '{}'. Response body '{}' from device {} for command {}", - httpResponse.statusCode(), httpResponse.body(), - commandData.getTargetDeviceId(), command); - sendResponseToApplication(command, - commandData.getTargetDeviceId(), - httpResponse, - receivedCommandContext.getCurrentSpan().context()); - return Future.succeededFuture(); - }).otherwise(sendCommandFailure -> { - LOG.error( - "Error sending command to device {}. Sending error response " - + "to application with code [{}]", - commandData.getTargetDeviceId(), - HttpURLConnection.HTTP_INTERNAL_ERROR, sendCommandFailure); - - TracingHelper.logError(receivedCommandContext.getCurrentSpan(), - sendCommandFailure); - sendResponseToApplication(command, loraDeviceId, - getHttpResponseWithCode(HttpURLConnection.HTTP_INTERNAL_ERROR, - sendCommandFailure.getMessage()), - receivedCommandContext.getCurrentSpan().context()); - return null; - }); - return Future.succeededFuture(); - }).otherwise(registrationAssertionFailure -> { - LOG.error("Error asserting device registration. Sending error response to " - + "application with code [{}]", HttpURLConnection.HTTP_FORBIDDEN, - registrationAssertionFailure); - TracingHelper.logError(receivedCommandContext.getCurrentSpan(), - registrationAssertionFailure); - sendResponseToApplication(command, loraDeviceId, - getHttpResponseWithCode(HttpURLConnection.HTTP_FORBIDDEN, - registrationAssertionFailure.getMessage()), - receivedCommandContext.getCurrentSpan().context()); - return null; - })) - .otherwise(loraGatewayException -> { - LOG.error("Error getting lora device gateway. Sending error response to application with " - + "code [{}]", HttpURLConnection.HTTP_INTERNAL_ERROR, loraGatewayException); - TracingHelper.logError(receivedCommandContext.getCurrentSpan(), loraGatewayException); - sendResponseToApplication(command, loraDeviceId, - getHttpResponseWithCode(HttpURLConnection.HTTP_INTERNAL_ERROR, - loraGatewayException.getMessage()), - receivedCommandContext.getCurrentSpan().context()); - return null; - }); - - return Future.succeededFuture(); - } else { - LOG.debug("Got invalid command {} for actual lora device '{}'", command, - command.getApplicationProperties().get(MessageHelper.APP_PROPERTY_DEVICE_ID)); - return Future.failedFuture("Malformed command message."); - } - }).otherwise(validationException -> { - LOG.error("Error trying to send command '{}'", command, validationException); - TracingHelper.logError(receivedCommandContext.getCurrentSpan(), validationException); - receivedCommandContext - .reject(new ErrorCondition(Constants.AMQP_BAD_REQUEST, validationException.getMessage()), 1); - return null; - }); - } - - private static boolean isValidLoraCommand(final Command command) { - try { - final String payload = command.getPayload().toJsonObject() - .getString(LoraConstants.FIELD_LORA_DOWNLINK_PAYLOAD); - - if (payload == null) { - return false; - } - } catch (final ClassCastException | DecodeException e) { - return false; - } - - final Object loraDeviceIdObject = command.getApplicationProperties().get(MessageHelper.APP_PROPERTY_DEVICE_ID); - if (!(loraDeviceIdObject instanceof String)) { - return false; - } - return command.isValid(); - } - - private Future> sendCommandToDevice(final CommandData commandData) { - LOG.debug("Sending {} to device", commandData.getCommand()); - final Future> responseHandler = Future.future(); - - final Optional providerOptional = loraProviders.stream() - .filter(loraProvider -> loraProvider.getProviderName().equals(commandData.getLoraProvider())) - .findFirst(); - if (providerOptional.isPresent()) { - final LoraProvider provider = providerOptional.get(); - LOG.debug("Using LoraProvider [{}] to send command to gateway", provider.getProviderName()); - provider.sendDownlinkCommand(commandData.getGateway(), commandData.getGatewayCredentials(), - commandData.getTargetDeviceId(), commandData.getCommand()).setHandler(r -> { - if (r.succeeded()) { - LOG.debug("Successfully sent message to lora provider"); - responseHandler.complete(getHttpResponseWithCode(HttpURLConnection.HTTP_OK, "OK")); - } else { - LOG.error("Got error from lora provider", r.cause()); - responseHandler - .fail("Could not send command to lora provider. " + commandData.getCommand()); - } - }); - } else { - LOG.error("No lora provider found for [{}]", commandData.getLoraProvider()); - responseHandler.fail("No suitable lora provider found to send command" + commandData.getCommand()); - } - return responseHandler; - } - - private void sendResponseToApplication(final Command command, final String loraDeviceId, - final HttpResponse response, final SpanContext spanContext) { - final Span currentSpan = TracingHelper.buildFollowsFromSpan(tracer, spanContext, "upload Command response") - .ignoreActiveSpan() - .withTag(Tags.COMPONENT.getKey(), getTypeName()) - .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT) - .withTag(MessageHelper.APP_PROPERTY_TENANT_ID, command.getTenant()) - .withTag(TAG_LORA_DEVICE_ID, loraDeviceId) - .withTag(MessageHelper.APP_PROPERTY_DEVICE_ID, command.getDeviceId()) - .withTag(Constants.HEADER_COMMAND_RESPONSE_STATUS, response.statusCode()) - .withTag(Constants.HEADER_COMMAND_REQUEST_ID, command.getRequestId()).start(); - - final CommandResponse commandResponse = CommandResponse.from(command.getRequestId(), command.getTenant(), command.getDeviceId(), - response.body(), response.getHeader("Content-Type"), response.statusCode()); - if (commandResponse != null) { - sendCommandResponse(command.getTenant(), commandResponse, currentSpan.context()).map(delivery -> { - LOG.debug("delivered command response [command-request-id: {}] to application", command.getRequestId()); - currentSpan.log("delivered command response to application"); - return delivery; - }).otherwise(error -> { - LOG.debug("could not send command response [command-request-id: {}] to application", - command.getRequestId(), error); - TracingHelper.logError(currentSpan, error); - return null; - }).setHandler(c -> currentSpan.finish()); - } else { - final String errorMsg = String.format("command-request-id [%s] or status code [%s] is missing/invalid", - command.getRequestId(), response.statusCode()); - LOG.debug("cannot send command response from lora device [{}] to application: {}", loraDeviceId, errorMsg); - TracingHelper.logError(currentSpan, errorMsg); - currentSpan.finish(); - } - } - - private Future getLoraDeviceGateway(final String tenantId, final String loraDeviceId) { - return getRegistrationClient(tenantId).compose(registrationClient -> registrationClient.get(loraDeviceId)) - .compose(this::extractGatewayId).compose(gatewayId -> getRegistrationClient(tenantId) - .compose(registrationClient -> registrationClient.get(gatewayId))); - } - - private Future getGatewayCredentials(final String tenantId, final JsonObject data) { - final String authId = data.getString(LoraConstants.FIELD_AUTH_ID); - return getCredentialsClientFactory().getOrCreateCredentialsClient(tenantId) - .compose(credentialsClient -> credentialsClient.get(LoraConstants.FIELD_PSK, authId)); - } - - private Future extractGatewayId(final JsonObject actualDevice) { - LOG.debug("Retrieved device from device-registry {}", actualDevice); - final JsonObject data = actualDevice.getJsonObject(RegistrationConstants.FIELD_DATA); - final String gatewayId = data.getString(LoraConstants.FIELD_VIA); - if (LoraUtils.isBlank(gatewayId)) { - LOG.error("Lora device has no gateway configured :{}", gatewayId); - return Future.failedFuture("Lora device has no gateway configured"); - } else { - LOG.debug("Successfully retrieved the gateway Id: {}", gatewayId); - return Future.succeededFuture(gatewayId); - } - } - - private HttpResponse getHttpResponseWithCode(final int statusCode, final String message) { - return new HttpResponse<>() { - - @Override - public HttpVersion version() { - return null; - } - - @Override - public int statusCode() { - return statusCode; - } - - @Override - public String statusMessage() { - return message; - } - - @Override - public MultiMap headers() { - return null; - } - - @Override - public String getHeader(final String headerName) { - return "application/json"; - } - - @Override - public MultiMap trailers() { - return null; - } - - @Override - public String getTrailer(final String trailerName) { - return null; - } - - @Override - public List cookies() { - return Collections.emptyList(); - } - - @Override - public Buffer body() { - return Buffer.buffer("command response"); - } - - @Override - public Buffer bodyAsBuffer() { - return null; - } - - @Override - public JsonArray bodyAsJsonArray() { - return null; - } - }; - } - - /** - * Data sent as command. - */ - static class CommandData { - - private JsonObject gateway; - private CredentialsObject gatewayCredentials; - private String loraProvider; - private Command command; - private String targetDeviceId; - - JsonObject getGateway() { - return gateway; - } - - /** - * sets the gateway and returns the lora network data contained in it. - * - * @param gateway target gateway - * @return Future containing lora network data if present, otherwise a failed future is returned. - */ - Future setGatewayAndExtractLoraNetworkData(final JsonObject gateway) { - this.gateway = gateway; - - if (LoraUtils.isValidLoraGateway(gateway)) { - final JsonObject loraNetworkData = LoraUtils.getLoraConfigFromLoraGatewayDevice(gateway); - this.loraProvider = loraNetworkData.getString(LoraConstants.FIELD_LORA_PROVIDER); - return Future.succeededFuture(loraNetworkData); - } else { - LOG.debug("Not a valid lora gateway configuration"); - return Future.failedFuture("Not a valid lora gateway configuration"); - } - } - - CredentialsObject getGatewayCredentials() { - return gatewayCredentials; - } - - void setGatewayCredentials(final CredentialsObject gatewayCredentials) { - this.gatewayCredentials = gatewayCredentials; - } - - String getLoraProvider() { - return loraProvider; - } - - Command getCommand() { - return command; - } - - void setCommand(final Command command) { - this.command = command; - } - - String getTargetDeviceId() { - return targetDeviceId; - } - - void setTargetDeviceId(final String targetDeviceId) { - this.targetDeviceId = targetDeviceId; - } - - } } diff --git a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/KerlinkProvider.java b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/KerlinkProvider.java index 4fdab46e17..c1f4d9db7a 100644 --- a/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/KerlinkProvider.java +++ b/adapters/lora-vertx/src/main/java/org/eclipse/hono/adapter/lora/providers/KerlinkProvider.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019, 2019 Contributors to the Eclipse Foundation + * Copyright (c) 2019, 2020 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -13,35 +13,9 @@ package org.eclipse.hono.adapter.lora.providers; -import java.net.HttpURLConnection; -import java.text.MessageFormat; -import java.time.Instant; -import java.util.Base64; -import java.util.List; - -import org.eclipse.hono.adapter.lora.LoraConstants; -import org.eclipse.hono.adapter.lora.impl.LoraProtocolAdapter; -import org.eclipse.hono.cache.ExpiringValueCache; -import org.eclipse.hono.client.Command; -import org.eclipse.hono.service.cache.SpringBasedExpiringValueCache; -import org.eclipse.hono.util.Constants; -import org.eclipse.hono.util.CredentialsObject; -import org.eclipse.hono.util.RequestResponseApiConstants; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cache.CacheManager; import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; -import io.vertx.core.Future; -import io.vertx.core.Vertx; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.json.DecodeException; import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.client.HttpResponse; -import io.vertx.ext.web.client.WebClient; -import io.vertx.ext.web.client.WebClientOptions; /** * A LoRaWAN provider with API for Kerlink. @@ -52,60 +26,12 @@ public class KerlinkProvider implements LoraProvider { static final String FIELD_KERLINK_CLUSTER_ID = "cluster-id"; static final String FIELD_KERLINK_CUSTOMER_ID = "customer-id"; - private static final Logger LOG = LoggerFactory.getLogger(LoraProtocolAdapter.class); - private static final String HEADER_CONTENT_TYPE_KERLINK_JSON = "application/vnd.kerlink.iot-v1+json"; - private static final String HEADER_BEARER_TOKEN = "Bearer"; - - // Cached tokens will be invalidated already earlier than required to avoid edge cases. - private static final int DEFAULT_DOWNLINK_TOKEN_PREEMPTIVE_INVALIDATION_TIME_IN_MS = 30_000; - - private static final String API_PATH_GET_TOKEN = "/oss/application/login"; - private static final String API_PATH_TX_MESSAGE = "/oss/application/customers/{0}/clusters/{1}/endpoints/{2}/txMessages"; private static final String FIELD_UPLINK_DEVICE_EUI = "devEui"; private static final String FIELD_UPLINK_USER_DATA = "userdata"; private static final String FIELD_UPLINK_PAYLOAD = "payload"; - private static final String FIELD_DOWNLINK_PORT = "port"; - private static final String FIELD_DOWNLINK_PAYLOAD = "payload"; - private static final String FIELD_DOWNLINK_CONTENT_TYPE = "contentType"; - private static final String FIELD_DOWNLINK_ACK = "ack"; - - private static final String VALUE_DOWNLINK_CONTENT_TYPE_HEXA = "HEXA"; - - private static final String FIELD_KERLINK_AUTH_LOGIN = "login"; - - private static final String FIELD_KERLINK_AUTH_PASSWORD = "password"; - - private static final String FIELD_KERLINK_EXPIRY_DATE = "expiredDate"; - private static final String FIELD_KERLINK_TOKEN = "token"; - - private final CacheManager cacheManager; - - private final ExpiringValueCache sessionsCache; - private int tokenPreemptiveInvalidationTimeInMs = DEFAULT_DOWNLINK_TOKEN_PREEMPTIVE_INVALIDATION_TIME_IN_MS; - - private final WebClient webClient; - - /** - * Creates a Kerlink provider with the given vertx instance and cache manager. - * - * @param vertx the vertx instance this provider should run on - * @param cacheManager the cache manager this provider should use - */ - @Autowired - public KerlinkProvider(final Vertx vertx, final CacheManager cacheManager) { - this.cacheManager = cacheManager; - - sessionsCache = new SpringBasedExpiringValueCache<>(cacheManager.getCache(KerlinkProvider.class.getName())); - - final WebClientOptions options = new WebClientOptions(); - options.setTrustAll(true); - - this.webClient = WebClient.create(vertx, options); - } - @Override public String getProviderName() { return "kerlink"; @@ -130,271 +56,4 @@ public String extractDeviceId(final JsonObject loraMessage) { public String extractPayload(final JsonObject loraMessage) { return loraMessage.getJsonObject(FIELD_UPLINK_USER_DATA, new JsonObject()).getString(FIELD_UPLINK_PAYLOAD); } - - @Override - public Future sendDownlinkCommand(final JsonObject gatewayDevice, final CredentialsObject gatewayCredential, - final String targetDeviceId, final Command loraCommand) { - LOG.info("Send downlink command for device '{}' using gateway '{}'", targetDeviceId, - gatewayDevice.getString(RequestResponseApiConstants.FIELD_PAYLOAD_DEVICE_ID)); - - if (!isValidDownlinkKerlinkGateway(gatewayDevice)) { - LOG.info( - "Can't send downlink command for device '{}' using gateway '{}' because of invalid gateway configuration.", - targetDeviceId, gatewayDevice.getString(RequestResponseApiConstants.FIELD_PAYLOAD_DEVICE_ID)); - return Future.failedFuture(new LoraProviderDownlinkException("LoRa configuration is not valid.")); - } - - final Future apiToken = getApiTokenFromCacheOrIssueNewFromLoraProvider(gatewayDevice, - gatewayCredential); - - return apiToken.compose(token -> { - LOG.info("Sending downlink command via rest api for device '{}' using gateway '{}' and resolved token", - targetDeviceId, gatewayDevice.getString(RequestResponseApiConstants.FIELD_PAYLOAD_DEVICE_ID)); - - final JsonObject loraPayload = loraCommand.getPayload().toJsonObject(); - - final String payloadBase64 = loraPayload.getString(LoraConstants.FIELD_LORA_DOWNLINK_PAYLOAD); - - final String payloadHex = LoraUtils.convertFromBase64ToHex(payloadBase64); - return sendDownlinkViaRest(token, gatewayDevice, targetDeviceId, payloadHex); - }); - } - - private Future sendDownlinkViaRest(final String bearerToken, final JsonObject gatewayDevice, - final String targetDevice, final String payloadHexa) { - LOG.debug("Invoking downlink rest api for device '{}'", targetDevice); - - final Future result = Future.future(); - - final String targetUri = getDownlinkRequestUri(gatewayDevice, targetDevice); - - final JsonObject loraProperties = LoraUtils.getLoraConfigFromLoraGatewayDevice(gatewayDevice); - final int port = loraProperties.getInteger(LoraConstants.FIELD_LORA_DEVICE_PORT); - - final JsonObject txMessage = new JsonObject(); - txMessage.put(FIELD_DOWNLINK_PORT, port); - txMessage.put(FIELD_DOWNLINK_PAYLOAD, payloadHexa); - txMessage.put(FIELD_DOWNLINK_CONTENT_TYPE, VALUE_DOWNLINK_CONTENT_TYPE_HEXA); - txMessage.put(FIELD_DOWNLINK_ACK, false); - - webClient.postAbs(targetUri).putHeader("content-type", HEADER_CONTENT_TYPE_KERLINK_JSON) - .putHeader("Authorization", HEADER_BEARER_TOKEN + " " + bearerToken) - .sendJsonObject(txMessage, response -> { - if (response.succeeded() && LoraUtils.isHttpSuccessStatusCode(response.result().statusCode())) { - LOG.debug("downlink rest api call for device '{}' was successful.", targetDevice); - result.complete(); - } else if (response.succeeded() && response.result().statusCode() == HttpURLConnection.HTTP_UNAUTHORIZED) { - LOG.debug( - "downlink rest api call for device '{}' failed because it was unauthorized. Response Body: '{}'", - targetDevice, response.result().bodyAsString()); - invalidateCacheForGatewayDevice(gatewayDevice); - result.fail(new LoraProviderDownlinkException( - "Error invoking downlink provider api. Request was unauthorized.")); - } else if (response.succeeded()) { - LOG.debug( - "Downlink rest api call for device '{}' returned unexpected status '{}'. Response Body: '{}'", - targetDevice, response.result().statusCode(), response.result().bodyAsString()); - result.fail(new LoraProviderDownlinkException( - "Error invoking downlink provider api. Response Code of provider api was: " - + response.result().statusCode())); - } else { - LOG.debug("Error invoking downlink rest api for device '{}'", targetDevice, response.cause()); - result.fail(new LoraProviderDownlinkException("Error invoking downlink provider api.", - response.cause())); - } - }); - - return result; - } - - private String getDownlinkRequestUri(final JsonObject gatewayDevice, final String targetDevice) { - final String hostName = LoraUtils.getNormalizedProviderUrlFromGatewayDevice(gatewayDevice); - final JsonObject vendorProperties = LoraUtils.getLoraConfigFromLoraGatewayDevice(gatewayDevice) - .getJsonObject(LoraConstants.FIELD_LORA_VENDOR_PROPERTIES); - final int customerId = vendorProperties.getInteger(FIELD_KERLINK_CUSTOMER_ID); - final int clusterId = vendorProperties.getInteger(FIELD_KERLINK_CLUSTER_ID); - - final String txUrlPath = MessageFormat.format(API_PATH_TX_MESSAGE, customerId, clusterId, targetDevice); - final String targetUrl = hostName + txUrlPath; - - LOG.debug("Invoking downlink rest api using url '{}' for device '{}'", targetUrl, targetDevice); - - return targetUrl; - } - - private Future getApiTokenFromCacheOrIssueNewFromLoraProvider(final JsonObject gatewayDevice, - final CredentialsObject gatewayCredentials) { - LOG.debug("A bearer token for gateway device '{}' with auth-id '{}' was requested", - gatewayDevice.getString(RequestResponseApiConstants.FIELD_PAYLOAD_DEVICE_ID), gatewayCredentials.getAuthId()); - - final String bearerToken = getCachedTokenForGatewayDevice(gatewayDevice); - - if (StringUtils.isEmpty(bearerToken)) { - LOG.debug("No bearer token for gateway device '{}' and auth-id '{}' in cache. Will request a new one", - gatewayDevice.getString(RequestResponseApiConstants.FIELD_PAYLOAD_DEVICE_ID), gatewayCredentials.getAuthId()); - - return getApiTokenFromLoraProvider(gatewayDevice, gatewayCredentials).compose(apiResponse -> { - LOG.debug("Got bearer token for gateway device '{}' and auth-id '{}'.", - gatewayDevice.getString(RequestResponseApiConstants.FIELD_PAYLOAD_DEVICE_ID), gatewayCredentials.getAuthId()); - - final String token = apiResponse.getString(FIELD_KERLINK_TOKEN); - final Long tokenExpiryString = apiResponse.getLong(FIELD_KERLINK_EXPIRY_DATE); - final Instant tokenExpiry = Instant.ofEpochMilli(tokenExpiryString) - .minusMillis(getTokenPreemptiveInvalidationTimeInMs()); - - if (Instant.now().isBefore(tokenExpiry)) { - putTokenForGatewayDeviceToCache(gatewayDevice, token, tokenExpiry); - } - - return Future.succeededFuture(token); - }); - } else { - LOG.debug("Bearer token for gateway device '{}' and auth-id '{}' is in cache.", - gatewayDevice.getString(RequestResponseApiConstants.FIELD_PAYLOAD_DEVICE_ID), gatewayCredentials.getAuthId()); - return Future.succeededFuture(bearerToken); - } - } - - private Future getApiTokenFromLoraProvider(final JsonObject gatewayDevice, - final CredentialsObject gatewayCredentials) { - final List currentlyValidSecrets = gatewayCredentials.getCandidateSecrets(); - - LOG.debug("Got a total of {} valid secrets for gateway device '{}' and auth-id '{}'", - currentlyValidSecrets.size(), gatewayDevice.getString(RequestResponseApiConstants.FIELD_PAYLOAD_DEVICE_ID), - gatewayCredentials.getAuthId()); - - // For now we didn't implement support for multiple valid secrets at the same time. - final JsonObject currentSecret = currentlyValidSecrets.get(0); - - return requestApiTokenWithSecret(gatewayDevice, currentSecret); - } - - private Future requestApiTokenWithSecret(final JsonObject gatewayDevice, final JsonObject secret) { - final Future result = Future.future(); - - final String loginUri = LoraUtils.getNormalizedProviderUrlFromGatewayDevice(gatewayDevice) + API_PATH_GET_TOKEN; - - final String passwordBase64 = secret.getString(LoraConstants.FIELD_LORA_CREDENTIAL_KEY); - final String password = new String(Base64.getDecoder().decode(passwordBase64)); - - final JsonObject loginRequestPayload = new JsonObject(); - loginRequestPayload.put(FIELD_KERLINK_AUTH_LOGIN, secret.getString(LoraConstants.FIELD_LORA_CREDENTIAL_IDENTITY)); - loginRequestPayload.put(FIELD_KERLINK_AUTH_PASSWORD, password); - - LOG.debug("Going to obtain token for gateway device '{}' using url: '{}'", - gatewayDevice.getString(RequestResponseApiConstants.FIELD_PAYLOAD_DEVICE_ID), loginUri); - - webClient.postAbs(loginUri).putHeader("content-type", HEADER_CONTENT_TYPE_KERLINK_JSON) - .sendJsonObject(loginRequestPayload, response -> { - if (response.succeeded() && validateTokenResponse(response.result())) { - result.complete(response.result().bodyAsJsonObject()); - } else { - LOG.debug("Error obtaining token for gateway device '{}' using url: '{}'", - gatewayDevice.getString(RequestResponseApiConstants.FIELD_PAYLOAD_DEVICE_ID), loginUri); - result.fail(new LoraProviderDownlinkException("Could not get authentication token for provider", - response.cause())); - } - }); - return result; - } - - private String getCachedTokenForGatewayDevice(final JsonObject gatewayDevice) { - final String cacheId = getCacheIdForGatewayDevice(gatewayDevice); - return sessionsCache.get(cacheId); - } - - private void putTokenForGatewayDeviceToCache(final JsonObject gatewayDevice, final String token, - final Instant expiryDate) { - final String cacheId = getCacheIdForGatewayDevice(gatewayDevice); - LOG.debug("Going to put token to cache with id '{}'", cacheId); - sessionsCache.put(cacheId, token, expiryDate); - } - - private void invalidateCacheForGatewayDevice(final JsonObject gatewayDevice) { - final String cacheId = getCacheIdForGatewayDevice(gatewayDevice); - LOG.debug("Invalidating item in cache with id '{}'", cacheId); - // Ugly to directly remove it from the underlying cache, but Hono cache does not implement evict method yet. - cacheManager.getCache(KerlinkProvider.class.getName()).evict(cacheId); - } - - private String getCacheIdForGatewayDevice(final JsonObject gatewayDevice) { - return String.format("%s_%s_%s", - gatewayDevice.getString(Constants.JSON_FIELD_TENANT_ID), - gatewayDevice.getString(Constants.JSON_FIELD_DEVICE_ID), - LoraUtils.getLoraConfigFromLoraGatewayDevice(gatewayDevice).getString(LoraConstants.FIELD_AUTH_ID)); - } - - private boolean isValidDownlinkKerlinkGateway(final JsonObject gatewayDevice) { - final JsonObject loraConfig = LoraUtils.getLoraConfigFromLoraGatewayDevice(gatewayDevice); - if (loraConfig == null) { - return false; - } - - final JsonObject vendorProperties = loraConfig.getJsonObject(LoraConstants.FIELD_LORA_VENDOR_PROPERTIES); - if (vendorProperties == null) { - return false; - } - - if (vendorProperties.getInteger(FIELD_KERLINK_CUSTOMER_ID) == null) { - return false; - } - - if (vendorProperties.getInteger(FIELD_KERLINK_CLUSTER_ID) == null) { - return false; - } - - return true; - } - - private boolean validateTokenResponse(final HttpResponse response) { - if (!LoraUtils.isHttpSuccessStatusCode(response.statusCode())) { - LOG.debug("Received non success status code: '{}' from api.", response.statusCode()); - return false; - } - - final JsonObject apiResponse; - - try { - apiResponse = response.bodyAsJsonObject(); - } catch (final DecodeException e) { - LOG.debug("Received non json object from api with data.", e); - return false; - } - - final String token; - try { - token = apiResponse.getString(FIELD_KERLINK_TOKEN); - } catch (final ClassCastException e) { - LOG.debug("Received token with invalid syntax from api."); - return false; - } - - if (StringUtils.isEmpty(token)) { - LOG.debug("Received token with invalid syntax from api."); - return false; - } - - final Long expiryDate; - try { - expiryDate = apiResponse.getLong(FIELD_KERLINK_EXPIRY_DATE); - } catch (final ClassCastException e) { - LOG.debug("Received expiry date with invalid syntax from api."); - return false; - } - - if (expiryDate == null) { - LOG.debug("Received token without expiryDate from api."); - return false; - } - - return true; - } - - int getTokenPreemptiveInvalidationTimeInMs() { - return tokenPreemptiveInvalidationTimeInMs; - } - - void setTokenPreemptiveInvalidationTimeInMs(final int time) { - this.tokenPreemptiveInvalidationTimeInMs = time; - } } diff --git a/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/KerlinkProviderTest.java b/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/KerlinkProviderTest.java index 94b71ea347..c06c5db1dc 100644 --- a/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/KerlinkProviderTest.java +++ b/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/KerlinkProviderTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019, 2019 Contributors to the Eclipse Foundation + * Copyright (c) 2019, 2020 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -13,67 +13,39 @@ package org.eclipse.hono.adapter.lora.providers; -import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.junit.jupiter.api.Assertions.assertEquals; -import java.time.Instant; -import java.util.Base64; - -import org.apache.qpid.proton.amqp.messaging.AmqpValue; -import org.apache.qpid.proton.message.Message; -import org.eclipse.hono.adapter.lora.LoraConstants; import org.eclipse.hono.adapter.lora.LoraMessageType; -import org.eclipse.hono.client.Command; -import org.eclipse.hono.util.CommandConstants; -import org.eclipse.hono.util.CredentialsObject; -import org.eclipse.hono.util.RegistrationConstants; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.cache.concurrent.ConcurrentMapCacheManager; - -import com.github.tomakehurst.wiremock.core.WireMockConfiguration; -import com.github.tomakehurst.wiremock.http.Fault; -import com.github.tomakehurst.wiremock.junit.WireMockRule; -import com.google.common.base.Charsets; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.ExtendWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import io.vertx.core.Vertx; import io.vertx.core.json.JsonObject; -import io.vertx.ext.unit.Async; -import io.vertx.ext.unit.TestContext; -import io.vertx.ext.unit.junit.VertxUnitRunner; +import io.vertx.junit5.VertxExtension; /** * Verifies the behavior of {@link KerlinkProvider}. */ -@RunWith(VertxUnitRunner.class) +@ExtendWith(VertxExtension.class) public class KerlinkProviderTest { - private static final String KERLINK_APPLICATION_TYPE = "application/vnd.kerlink.iot-v1+json"; - - private static final String TEST_KERLINK_API_USER = "kerlinkApiUser"; - private static final String TEST_KERLINK_API_PASSWORD = "kerlinkApiPassword"; - - private static final String KERLINK_URL_TOKEN = "/oss/application/login"; - private static final String KERLINK_URL_DOWNLINK = "/oss/application/customers/.*/clusters/.*/endpoints/.*/txMessages"; - - @Rule - public WireMockRule wireMockRule = new WireMockRule(WireMockConfiguration.wireMockConfig().dynamicPort().dynamicPort()); + private static final Logger LOG = LoggerFactory.getLogger(KerlinkProviderTest.class); private KerlinkProvider provider; - - private final Vertx vertx = Vertx.vertx(); - /** * Sets up the fixture. + * + * @param testInfo The test meta data. */ - @Before - public void before() { - provider = new KerlinkProvider(vertx, new ConcurrentMapCacheManager()); - // Use very small value here to avoid long running unit tests. - provider.setTokenPreemptiveInvalidationTimeInMs(100); + @BeforeEach + public void before(final TestInfo testInfo) { + + LOG.info("running test: {}", testInfo.getDisplayName()); + provider = new KerlinkProvider(); } /** @@ -84,7 +56,7 @@ public void extractDeviceIdFromLoraMessage() { final JsonObject loraMessage = LoraTestUtil.loadTestFile("kerlink.uplink"); final String deviceId = provider.extractDeviceId(loraMessage); - Assert.assertEquals("myBumluxDevice", deviceId); + assertEquals("myBumluxDevice", deviceId); } /** @@ -95,7 +67,7 @@ public void extractPayloadFromLoraMessage() { final JsonObject loraMessage = LoraTestUtil.loadTestFile("kerlink.uplink"); final String payload = provider.extractPayload(loraMessage); - Assert.assertEquals("YnVtbHV4", payload); + assertEquals("YnVtbHV4", payload); } /** @@ -105,410 +77,6 @@ public void extractPayloadFromLoraMessage() { public void extractTypeFromLoraUplinkMessage() { final JsonObject loraMessage = LoraTestUtil.loadTestFile("kerlink.uplink"); final LoraMessageType type = provider.extractMessageType(loraMessage); - Assert.assertEquals(LoraMessageType.UPLINK, type); - } - - /** - * Verifies that sending a downlink command is successful. - * - * @param context The helper to use for running async tests on vertx. - */ - @Test - public void sendingDownlinkCommandIsSuccessful(final TestContext context) { - final Async async = context.async(); - stubSuccessfulTokenRequest(); - stubSuccessfulDownlinkRequest(); - - final JsonObject loraGatewayDevice = getValidGatewayDevice(); - final CredentialsObject gatewayCredential = getValidGatewayCredential(); - - final String targetDeviceId = "myTestDevice"; - final Command command = getValidDownlinkCommand(); - - provider.sendDownlinkCommand(loraGatewayDevice, gatewayCredential, targetDeviceId, command) - .setHandler(downlinkResult -> { - context.assertTrue(downlinkResult.succeeded()); - - final JsonObject expectedBody = new JsonObject(); - expectedBody.put("login", TEST_KERLINK_API_USER); - expectedBody.put("password", TEST_KERLINK_API_PASSWORD); - - verify(postRequestedFor(urlEqualTo("/oss/application/login")) - .withRequestBody(equalToJson(expectedBody.encode()))); - - async.complete(); - }); - } - - /** - * Verifies that sending a downlink fails when LoRa config is missing. - * - * @param context The helper to use for running async tests on vertx. - */ - @Test - public void sendingDownlinkFailsOnMissingLoraConfig(final TestContext context) { - final JsonObject loraGatewayDevice = getValidGatewayDevice(); - loraGatewayDevice.getJsonObject(RegistrationConstants.FIELD_DATA).remove(LoraConstants.FIELD_LORA_CONFIG); - - expectValidationFailureForGateway(context, loraGatewayDevice); - } - - /** - * Verifies that sending a downlink fails when vendor properties are missing. - * - * @param context The helper to use for running async tests on vertx. - */ - @Test - public void sendingDownlinkFailsOnMissingVendorProperties(final TestContext context) { - final JsonObject loraGatewayDevice = getValidGatewayDevice(); - final JsonObject loraConfig = LoraUtils.getLoraConfigFromLoraGatewayDevice(loraGatewayDevice); - loraConfig.remove(LoraConstants.FIELD_LORA_VENDOR_PROPERTIES); - expectValidationFailureForGateway(context, loraGatewayDevice); - } - - /** - * Verifies that sending a downlink fails when the cluster id is missing. - * - * @param context The helper to use for running async tests on vertx. - */ - @Test - public void sendingDownlinkFailsOnMissingClusterId(final TestContext context) { - final JsonObject loraGatewayDevice = getValidGatewayDevice(); - LoraUtils.getLoraConfigFromLoraGatewayDevice(loraGatewayDevice) - .getJsonObject(LoraConstants.FIELD_LORA_VENDOR_PROPERTIES) - .remove(KerlinkProvider.FIELD_KERLINK_CLUSTER_ID); - - expectValidationFailureForGateway(context, loraGatewayDevice); - } - - /** - * Verifies that sending a downlink fails when the customer id is missing. - * - * @param context The helper to use for running async tests on vertx. - */ - @Test - public void sendingDownlinkFailsOnMissingCustomerId(final TestContext context) { - final JsonObject loraGatewayDevice = getValidGatewayDevice(); - LoraUtils.getLoraConfigFromLoraGatewayDevice(loraGatewayDevice) - .getJsonObject(LoraConstants.FIELD_LORA_VENDOR_PROPERTIES) - .remove(KerlinkProvider.FIELD_KERLINK_CUSTOMER_ID); - - expectValidationFailureForGateway(context, loraGatewayDevice); - } - - private void expectValidationFailureForGateway(final TestContext context, final JsonObject loraGatewayDevice) { - final Async async = context.async(); - - final CredentialsObject gatewayCredential = getValidGatewayCredential(); - - final String targetDeviceId = "myTestDevice"; - final Command command = getValidDownlinkCommand(); - - provider.sendDownlinkCommand(loraGatewayDevice, gatewayCredential, targetDeviceId, command) - .setHandler(downlinkResult -> { - context.assertTrue(downlinkResult.failed()); - - async.complete(); - }); - } - - /** - * Verifies that a token is renewed after the token has expired. - * - * @param context The helper to use for running async tests on vertx. - */ - @Test - public void tokenIsRenewedAfterTokenExpiry(final TestContext context) { - final Async async = context.async(); - stubSuccessfulTokenRequest(Instant.now().plusMillis(250)); - stubSuccessfulDownlinkRequest(); - - final JsonObject loraGatewayDevice = getValidGatewayDevice(); - final CredentialsObject gatewayCredential = getValidGatewayCredential(); - - final String targetDeviceId = "myTestDevice"; - final Command command = getValidDownlinkCommand(); - - provider.sendDownlinkCommand(loraGatewayDevice, gatewayCredential, targetDeviceId, command) - .setHandler(firstResponse -> { - context.assertTrue(firstResponse.succeeded()); - - vertx.setTimer(500, - nextRequest -> provider - .sendDownlinkCommand(loraGatewayDevice, gatewayCredential, targetDeviceId, command) - .setHandler(secondResponse -> { - context.assertTrue(secondResponse.succeeded()); - LoraTestUtil.verifyAsync(context, 2, postRequestedFor(urlEqualTo("/oss/application/login"))); - async.complete(); - })); - }); - } - - /** - * Verifies that a token is reused from cache while it is still valid. - * - * @param context The helper to use for running async tests on vertx. - */ - @Test - public void tokenIsReusedFromCacheWhileValid(final TestContext context) { - final Async async = context.async(); - stubSuccessfulTokenRequest(); - stubSuccessfulDownlinkRequest(); - - final JsonObject loraGatewayDevice = getValidGatewayDevice(); - final CredentialsObject gatewayCredential = getValidGatewayCredential(); - - final String targetDeviceId = "myTestDevice"; - final Command command = getValidDownlinkCommand(); - - provider.sendDownlinkCommand(loraGatewayDevice, gatewayCredential, targetDeviceId, command) - .setHandler(firstResponse -> { - context.assertTrue(firstResponse.succeeded()); - - vertx.setTimer(250, - nextRequest -> provider - .sendDownlinkCommand(loraGatewayDevice, gatewayCredential, targetDeviceId, command) - .setHandler(secondResponse -> { - context.assertTrue(secondResponse.succeeded()); - LoraTestUtil.verifyAsync(context, 1, postRequestedFor(urlEqualTo("/oss/application/login"))); - async.complete(); - })); - }); - } - - /** - * Verifies that a token is invalidated after an unauthorized downlink request. - * - * @param context The helper to use for running async tests on vertx. - */ - @Test - public void tokenIsInvalidatedOnApiUnauthorized(final TestContext context) { - final Async async = context.async(); - stubSuccessfulTokenRequest(); - stubUnauthorizedDownlinkRequest(); - - final JsonObject loraGatewayDevice = getValidGatewayDevice(); - final CredentialsObject gatewayCredential = getValidGatewayCredential(); - - final String targetDeviceId = "myTestDevice"; - final Command command = getValidDownlinkCommand(); - - provider.sendDownlinkCommand(loraGatewayDevice, gatewayCredential, targetDeviceId, command) - .setHandler(firstResponse -> { - context.assertTrue(firstResponse.failed()); - - provider.sendDownlinkCommand(loraGatewayDevice, gatewayCredential, targetDeviceId, command) - .setHandler(secondResponse -> { - LoraTestUtil.verifyAsync(context, 2, postRequestedFor(urlEqualTo("/oss/application/login"))); - async.complete(); - }); - }); - } - - /** - * Verifies that a request fails on invalid token response. - * - * @param context The helper to use for running async tests on vertx. - */ - @Test - public void failureOnInvalidTokenResponse(final TestContext context) { - final Async async = context.async(); - stubInvalidResponseOnTokenRequest(); - - final JsonObject loraGatewayDevice = getValidGatewayDevice(); - final CredentialsObject gatewayCredential = getValidGatewayCredential(); - - final String targetDeviceId = "myTestDevice"; - final Command command = getValidDownlinkCommand(); - - provider.sendDownlinkCommand(loraGatewayDevice, gatewayCredential, targetDeviceId, command) - .setHandler(downlinkResult -> { - context.assertTrue(downlinkResult.failed()); - async.complete(); - }); - } - - /** - * Verifies that a request fails on invalid token request. - * - * @param context The helper to use for running async tests on vertx. - */ - @Test - public void failureOnTokenRequest(final TestContext context) { - final Async async = context.async(); - stubFailureOnTokenRequest(); - - final JsonObject loraGatewayDevice = getValidGatewayDevice(); - final CredentialsObject gatewayCredential = getValidGatewayCredential(); - - final String targetDeviceId = "myTestDevice"; - final Command command = getValidDownlinkCommand(); - - provider.sendDownlinkCommand(loraGatewayDevice, gatewayCredential, targetDeviceId, command) - .setHandler(downlinkResult -> { - context.assertTrue(downlinkResult.failed()); - async.complete(); - }); - } - - /** - * Verifies that a failed downlink request is handled properly. - * - * @param context The helper to use for running async tests on vertx. - */ - @Test - public void failureOnDownlinkRequest(final TestContext context) { - final Async async = context.async(); - stubSuccessfulTokenRequest(); - stubFailureOnDownlinkRequest(); - - final JsonObject loraGatewayDevice = getValidGatewayDevice(); - final CredentialsObject gatewayCredential = getValidGatewayCredential(); - - final String targetDeviceId = "myTestDevice"; - final Command command = getValidDownlinkCommand(); - - provider.sendDownlinkCommand(loraGatewayDevice, gatewayCredential, targetDeviceId, command) - .setHandler(downlinkResult -> { - context.assertTrue(downlinkResult.failed()); - async.complete(); - }); - } - - /** - * Verifies that a connection fault on a downlink request is handled properly. - * - * @param context The helper to use for running async tests on vertx. - */ - @Test - public void faultOnDownlinkRequest(final TestContext context) { - final Async async = context.async(); - stubSuccessfulTokenRequest(); - stubConnectionFaultOnDownlinkRequest(); - - final JsonObject loraGatewayDevice = getValidGatewayDevice(); - final CredentialsObject gatewayCredential = getValidGatewayCredential(); - - final String targetDeviceId = "myTestDevice"; - final Command command = getValidDownlinkCommand(); - - provider.sendDownlinkCommand(loraGatewayDevice, gatewayCredential, targetDeviceId, command) - .setHandler(downlinkResult -> { - context.assertTrue(downlinkResult.failed()); - async.complete(); - }); - } - - private CredentialsObject getValidGatewayCredential() { - final JsonObject secret = new JsonObject(); - secret.put("identity", TEST_KERLINK_API_USER); - secret.put("key", Base64.getEncoder().encodeToString(TEST_KERLINK_API_PASSWORD.getBytes(Charsets.UTF_8))); - - final CredentialsObject gatewayCredential = new CredentialsObject(); - gatewayCredential.setAuthId("lora-secret"); - gatewayCredential.addSecret(secret); - - return gatewayCredential; - } - - private JsonObject getValidGatewayDevice() { - final JsonObject loraVendorProperties = new JsonObject(); - loraVendorProperties.put("cluster-id", 23); - loraVendorProperties.put("customer-id", 4); - - final JsonObject loraNetworkServerData = new JsonObject(); - loraNetworkServerData.put("provider", "kerlink"); - loraNetworkServerData.put("auth-id", "lora-secret"); - loraNetworkServerData.put("url", "http://localhost:" + wireMockRule.port()); - loraNetworkServerData.put("vendor-properties", loraVendorProperties); - loraNetworkServerData.put("lora-port", 23); - - final JsonObject loraGatewayData = new JsonObject(); - loraGatewayData.put("lora-network-server", loraNetworkServerData); - - final JsonObject loraGatewayDevice = new JsonObject(); - loraGatewayDevice.put("tenant-id", "test-tenant"); - loraGatewayDevice.put("device-id", "bumlux"); - loraGatewayDevice.put("enabled", true); - loraGatewayDevice.put("data", loraGatewayData); - - return loraGatewayDevice; - } - - private Command getValidDownlinkCommand() { - final Message message = Message.Factory.create(); - message.setSubject("subject"); - message.setCorrelationId("correlation_id"); - message.setReplyTo(CommandConstants.NORTHBOUND_COMMAND_RESPONSE_ENDPOINT + "/bumlux"); - - final JsonObject payload = new JsonObject(); - payload.put(LoraConstants.FIELD_LORA_DOWNLINK_PAYLOAD, "bumlux".getBytes(Charsets.UTF_8)); - - message.setBody(new AmqpValue(payload.encode())); - - return Command.from(message, "bumlux", "bumlux"); - } - - private void stubSuccessfulTokenRequest() { - final Instant tokenExpiryTime = Instant.now().plusSeconds(60); - stubSuccessfulTokenRequest(tokenExpiryTime); - } - - private void stubSuccessfulTokenRequest(final Instant tokenExpiryTime) { - final JsonObject result = new JsonObject(); - result.put("expiredDate", tokenExpiryTime.toEpochMilli()); - result.put("tokenType", "Bearer"); - result.put("token", "ThisIsAveryLongBearerTokenUsedByKerlink"); - - stubFor(post(urlEqualTo(KERLINK_URL_TOKEN)) - .withHeader("Content-Type", equalTo(KERLINK_APPLICATION_TYPE)) - .willReturn(aResponse() - .withStatus(201) - .withHeader("Content-Type", KERLINK_APPLICATION_TYPE) - .withBody(result.encodePrettily()))); - } - - private void stubFailureOnTokenRequest() { - stubFor(post(urlEqualTo(KERLINK_URL_TOKEN)) - .withHeader("Content-Type", equalTo(KERLINK_APPLICATION_TYPE)) - .willReturn(aResponse() - .withStatus(500) - .withHeader("Content-Type", KERLINK_APPLICATION_TYPE))); - } - - private void stubInvalidResponseOnTokenRequest() { - stubFor(post(urlEqualTo(KERLINK_URL_TOKEN)) - .withHeader("Content-Type", equalTo(KERLINK_APPLICATION_TYPE)) - .willReturn(aResponse() - .withStatus(201) - .withHeader("Content-Type", KERLINK_APPLICATION_TYPE) - .withBody("Here should be JSON, but instead it's some text we're for sure not expecting."))); - } - - private void stubSuccessfulDownlinkRequest() { - stubFor(post(urlPathMatching(KERLINK_URL_DOWNLINK)) - .withHeader("Content-Type", equalTo(KERLINK_APPLICATION_TYPE)) - .willReturn(aResponse() - .withStatus(201))); - } - - private void stubUnauthorizedDownlinkRequest() { - stubFor(post(urlPathMatching(KERLINK_URL_DOWNLINK)) - .withHeader("Content-Type", equalTo(KERLINK_APPLICATION_TYPE)) - .willReturn(aResponse() - .withStatus(401))); - } - - private void stubFailureOnDownlinkRequest() { - stubFor(post(urlPathMatching(KERLINK_URL_DOWNLINK)) - .withHeader("Content-Type", equalTo(KERLINK_APPLICATION_TYPE)) - .willReturn(aResponse() - .withStatus(500).withBody("Something went really wrong."))); - } - - private void stubConnectionFaultOnDownlinkRequest() { - stubFor(post(urlPathMatching(KERLINK_URL_DOWNLINK)) - .withHeader("Content-Type", equalTo(KERLINK_APPLICATION_TYPE)) - .willReturn(aResponse().withFault(Fault.MALFORMED_RESPONSE_CHUNK))); + assertEquals(LoraMessageType.UPLINK, type); } } diff --git a/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/LoraTestUtil.java b/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/LoraTestUtil.java index d29f1eb838..ffbb771768 100644 --- a/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/LoraTestUtil.java +++ b/adapters/lora-vertx/src/test/java/org/eclipse/hono/adapter/lora/providers/LoraTestUtil.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019, 2019 Contributors to the Eclipse Foundation + * Copyright (c) 2019, 2020 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -17,11 +17,7 @@ import java.nio.file.Files; import java.util.List; -import com.github.tomakehurst.wiremock.client.WireMock; -import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder; - import io.vertx.core.json.JsonObject; -import io.vertx.ext.unit.TestContext; /** * Utility methods for testing functionality of LoRa providers. @@ -49,34 +45,4 @@ public static JsonObject loadTestFile(final String name) throws RuntimeException throw new RuntimeException(e); } } - - /** - * Verifies a request matches the given pattern. - * - * @param context the corresponding context - * @param requestPatternBuilder the request pattern to match - */ - public static void verifyAsync(final TestContext context, final RequestPatternBuilder requestPatternBuilder) { - try { - WireMock.verify(requestPatternBuilder); - } catch (final AssertionError e) { - context.fail(e); - } - } - - /** - * Verifies multiple requests match the given pattern. - * - * @param context the corresponding context - * @param count the number of requests to match - * @param requestPatternBuilder the request pattern to match - */ - public static void verifyAsync(final TestContext context, final int count, - final RequestPatternBuilder requestPatternBuilder) { - try { - WireMock.verify(count, requestPatternBuilder); - } catch (final AssertionError e) { - context.fail(e); - } - } } diff --git a/client/src/main/java/org/eclipse/hono/client/RegistrationClient.java b/client/src/main/java/org/eclipse/hono/client/RegistrationClient.java index 0c33b14df5..d79b66fa9a 100644 --- a/client/src/main/java/org/eclipse/hono/client/RegistrationClient.java +++ b/client/src/main/java/org/eclipse/hono/client/RegistrationClient.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2019 Contributors to the Eclipse Foundation + * Copyright (c) 2016, 2020 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -101,47 +101,4 @@ default Future assertRegistration( return assertRegistration(deviceId, gatewayId); } - - /** - * Gets registration information for a device. - * - * @param deviceId The id of the device to check. - * @return A future indicating the result of the operation. - *

- * The future will succeed if a response with status 200 has been received from the - * registration service. The JSON object will then contain values as defined in - * - * Get Registration Information. - *

- * Otherwise, the future will fail with a {@link ServiceInvocationException} containing - * the (error) status code returned by the service. - * @throws NullPointerException if device ID is {@code null}. - * @see RequestResponseClient#setRequestTimeout(long) - */ - Future get(String deviceId); - - /** - * Gets registration information for a device. - *

- * This default implementation simply returns the result of {@link #get(String)}. - * - * @param deviceId The id of the device to check. - * @param context The currently active OpenTracing span. An implementation - * should use this as the parent for any span it creates for tracing - * the execution of this operation. - * @return A future indicating the result of the operation. - *

- * The future will succeed if a response with status 200 has been received from the registration service. - * The JSON object will then contain values as defined in - * Get - * Registration Information. - *

- * Otherwise, the future will fail with a {@link ServiceInvocationException} containing the (error) status - * code returned by the service. - * @throws NullPointerException if device ID is {@code null}. - * @see RequestResponseClient#setRequestTimeout(long) - */ - default Future get(String deviceId, final SpanContext context) { - return get(deviceId); - } } diff --git a/client/src/main/java/org/eclipse/hono/client/impl/RegistrationClientImpl.java b/client/src/main/java/org/eclipse/hono/client/impl/RegistrationClientImpl.java index 618a59c9b3..36e1cf33de 100644 --- a/client/src/main/java/org/eclipse/hono/client/impl/RegistrationClientImpl.java +++ b/client/src/main/java/org/eclipse/hono/client/impl/RegistrationClientImpl.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2019 Contributors to the Eclipse Foundation + * Copyright (c) 2016, 2020 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -164,45 +164,6 @@ private Map createDeviceIdProperties(final String deviceId) { return properties; } - /** - * Invokes the Get Registration Information operation of Hono's - * Device Registration API - * on the service represented by the sender and receiver links. - */ - @Override - public final Future get(final String deviceId) { - return get(deviceId, null); - } - - /** - * Invokes the Get Registration Information operation of Hono's - * Device Registration API - * on the service represented by the sender and receiver links. - */ - @Override - public final Future get(final String deviceId, final SpanContext context) { - - Objects.requireNonNull(deviceId); - final Future resultTracker = Future.future(); - - createAndSendRequest( - RegistrationConstants.ACTION_GET, - createDeviceIdProperties(deviceId), - null, - resultTracker, - null, - context); - - return resultTracker.map(regResult -> { - switch (regResult.getStatus()) { - case HttpURLConnection.HTTP_OK: - return regResult.getPayload(); - default: - throw StatusCodeMapper.from(regResult); - } - }); - } - /** * Invokes the Assert Device Registration operation of Hono's * Device Registration API diff --git a/site/homepage/content/release-notes.md b/site/homepage/content/release-notes.md index 61a19dcdb4..0bc2eb07ce 100644 --- a/site/homepage/content/release-notes.md +++ b/site/homepage/content/release-notes.md @@ -2,6 +2,15 @@ title = "Release Notes" +++ +## 1.0.4 + +### API Changes + +* The `org.eclipse.hono.client.DeviceRegistration` interface's *get* methods have been removed + because the Device Registration API does not define a corresponding operation. + Consequently, the C&C functionality of the Kerlink Lora provider which relied on the *get* + method has been removed. + ## 1.0.3 ### Fixes & Enhancements diff --git a/tests/src/test/java/org/eclipse/hono/tests/jms/JmsBasedRegistrationClient.java b/tests/src/test/java/org/eclipse/hono/tests/jms/JmsBasedRegistrationClient.java index b6da255352..4da48b7eca 100644 --- a/tests/src/test/java/org/eclipse/hono/tests/jms/JmsBasedRegistrationClient.java +++ b/tests/src/test/java/org/eclipse/hono/tests/jms/JmsBasedRegistrationClient.java @@ -147,14 +147,6 @@ public Future sendRequest( } } - /** - * {@inheritDoc} - */ - @Override - public Future get(final String deviceId) { - throw new UnsupportedOperationException("not implemented"); - } - /** * {@inheritDoc} */