Skip to content

Commit

Permalink
[#1228] throw new added LiveChannelConditionNotAllowedException when …
Browse files Browse the repository at this point in the history
…the "live-channel-condition" is used for non-ThingQueryCommands

Signed-off-by: Thomas Jaeckle <thomas.jaeckle@bosch.io>
  • Loading branch information
thjaeckle committed Dec 20, 2021
1 parent 996f866 commit ad1fefa
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,14 @@ private static Cache<CorrelationIdKey, ActorRef> createCache(final Duration fall
}

/**
* Puts the specified response receiver for the correlation ID of the signal's correlation ID.
* Caches the specified response receiver for the correlation ID of the signal's correlation ID.
*
* @param signal the signal to extract the correlation ID from used for the cache key.
* @param responseReceiver the ActorRef of the response receiver to cache for the correlation ID.
* @throws NullPointerException if any argument is {@code null}.
* @throws IllegalArgumentException if the headers of {@code signal} do not contain a correlation ID.
*/
public void putCommand(final Signal<?> signal, final ActorRef responseReceiver) {
public void cacheSignalResponseReceiver(final Signal<?> signal, final ActorRef responseReceiver) {
cache.put(getCorrelationIdKeyForInsertion(checkNotNull(signal, "signal")),
checkNotNull(responseReceiver, "responseReceiver"));
}
Expand Down Expand Up @@ -191,7 +191,7 @@ public <S extends Signal<?>, T> CompletionStage<T> insertResponseReceiverConflic
return setUniqueCorrelationIdForGlobalDispatching(signal, false)
.thenCompose(commandWithUniqueCorrelationId -> {
final ActorRef receiver = receiverCreator.apply(commandWithUniqueCorrelationId);
putCommand(commandWithUniqueCorrelationId, receiver);
cacheSignalResponseReceiver(commandWithUniqueCorrelationId, receiver);
return responseHandler.apply(commandWithUniqueCorrelationId, receiver);
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
import org.eclipse.ditto.things.model.ThingConstants;
import org.eclipse.ditto.things.model.ThingId;
import org.eclipse.ditto.things.model.signals.commands.ThingCommand;
import org.eclipse.ditto.things.model.signals.commands.exceptions.LiveChannelConditionNotAllowedException;
import org.eclipse.ditto.things.model.signals.commands.exceptions.PolicyIdNotAllowedException;
import org.eclipse.ditto.things.model.signals.commands.exceptions.PolicyInvalidException;
import org.eclipse.ditto.things.model.signals.commands.exceptions.ThingCommandToAccessExceptionRegistry;
Expand Down Expand Up @@ -330,6 +331,10 @@ private Contextual<WithDittoHeaders> enforceThingCommandByPolicyEnforcer(
doSmartChannelSelection(thingQueryCommand, response, startTime, enforcer))
);
}
} else if (commandWithReadSubjects.getDittoHeaders().getLiveChannelCondition().isPresent()) {
throw LiveChannelConditionNotAllowedException.newBuilder()
.dittoHeaders(commandWithReadSubjects.getDittoHeaders())
.build();
} else {
result = forwardToThingsShardRegion(commandWithReadSubjects);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,24 +73,24 @@ public void newInstanceWithZeroDurationThrowsException() {
}

@Test
public void putNullCommandThrowsException() {
public void cacheNullSignalThrowsException() {
final var underTest = ResponseReceiverCache.newInstance();

assertThatNullPointerException()
.isThrownBy(() -> underTest.putCommand(null, null))
.withMessage("The command must not be null!")
.isThrownBy(() -> underTest.cacheSignalResponseReceiver(null, null))
.withMessage("The signal must not be null!")
.withNoCause();
}

@Test
public void putNullResponseReceiverThrowsException() {
public void cacheNullResponseReceiverThrowsException() {
final var underTest = ResponseReceiverCache.newInstance();
final var command = Mockito.mock(Command.class);
Mockito.when(command.getDittoHeaders())
.thenReturn(DittoHeaders.newBuilder().correlationId(testNameCorrelationId.getCorrelationId()).build());

assertThatNullPointerException()
.isThrownBy(() -> underTest.putCommand(command, null))
.isThrownBy(() -> underTest.cacheSignalResponseReceiver(command, null))
.withMessage("The responseReceiver must not be null!")
.withNoCause();
}
Expand Down Expand Up @@ -135,7 +135,7 @@ public void getExistingEntryWithinExpiryReturnsResponseReceiver() {
final var underTest = ResponseReceiverCache.newInstance();

final var mockReceiver = Mockito.mock(ActorRef.class);
underTest.putCommand(command, mockReceiver);
underTest.cacheSignalResponseReceiver(command, mockReceiver);

final var cacheEntryFuture = underTest.get(correlationId.toString());

Expand All @@ -151,7 +151,7 @@ public void getPreviouslyPutEntryAfterExpiryReturnsEmptyOptional() throws Execut
.thenReturn(getDittoHeadersWithCorrelationIdAndTimeout(correlationId, expiry));
final var underTest = ResponseReceiverCache.newInstance();

underTest.putCommand(command, Mockito.mock(ActorRef.class));
underTest.cacheSignalResponseReceiver(command, Mockito.mock(ActorRef.class));

final var cacheEntryFuture = Awaitility.await("get expired cache entry")
.pollDelay(expiry.plusMillis(250L))
Expand Down Expand Up @@ -197,7 +197,7 @@ public void getEntriesWithDifferentExpiryReturnsExpected() {

final var underTest = ResponseReceiverCache.newInstance();
IntStream.range(0, expirySequence.size())
.forEach(index -> underTest.putCommand(commands.get(index), responseReceivers.get(index)));
.forEach(index -> underTest.cacheSignalResponseReceiver(commands.get(index), responseReceivers.get(index)));

Awaitility.await()
.pollDelay(shortExpiry.plusMillis(100L))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
import org.eclipse.ditto.things.model.ThingsModelFactory;
import org.eclipse.ditto.things.model.signals.commands.ThingCommand;
import org.eclipse.ditto.things.model.signals.commands.exceptions.FeatureNotModifiableException;
import org.eclipse.ditto.things.model.signals.commands.exceptions.LiveChannelConditionNotAllowedException;
import org.eclipse.ditto.things.model.signals.commands.exceptions.PolicyInvalidException;
import org.eclipse.ditto.things.model.signals.commands.exceptions.ThingConditionFailedException;
import org.eclipse.ditto.things.model.signals.commands.exceptions.ThingConditionInvalidException;
Expand Down Expand Up @@ -388,6 +389,49 @@ public void enforceConditionAndLiveChannelCondition() {
}};
}

@Test
public void enforceLiveChannelConditionOnModifyCommandFails() {
final PolicyId policyId = PolicyId.of("policy:id");
final JsonObject thing = newThingWithAttributeWithPolicyId(policyId)
.toBuilder()
.build();

final JsonObject policy = PoliciesModelFactory.newPolicyBuilder(policyId)
.setRevision(1L)
.forLabel("authorize-self")
.setSubject(GOOGLE, TestSetup.SUBJECT_ID)
.setGrantedPermissions(PoliciesResourceType.thingResource(JsonPointer.empty()),
Permissions.newInstance(Permission.READ, Permission.WRITE))
.build()
.toJson(FieldType.all());

final SudoRetrieveThingResponse sudoRetrieveThingResponse =
SudoRetrieveThingResponse.of(thing, DittoHeaders.empty());
final SudoRetrievePolicyResponse sudoRetrievePolicyResponse =
SudoRetrievePolicyResponse.of(policyId, policy, DittoHeaders.empty());

new TestKit(system) {{
mockEntitiesActorInstance.setReply(TestSetup.THING_SUDO, sudoRetrieveThingResponse);
mockEntitiesActorInstance.setReply(TestSetup.POLICY_SUDO, sudoRetrievePolicyResponse);

final ActorRef underTest = newEnforcerActor(getRef());

// WHEN: Live channel condition is set on an unreadable feature
final DittoHeaders dittoHeaders = headers().toBuilder()
.liveChannelCondition("exists(thingId)")
.build();

final ThingCommand<?> modifyCommand = getModifyCommand(dittoHeaders);
mockEntitiesActorInstance.setReply(modifyCommand);
underTest.tell(modifyCommand, getRef());

// THEN: The command is rejected
final DittoRuntimeException response = TestSetup.fishForMsgClass(this, DittoRuntimeException.class);
assertThat(response.getErrorCode()).isEqualTo(LiveChannelConditionNotAllowedException.ERROR_CODE);
assertThat(response.getHttpStatus()).isEqualTo(HttpStatus.METHOD_NOT_ALLOWED);
}};
}

@Test
public void acceptCreateByInlinePolicy() {
final PolicyId policyId = PolicyId.of(TestSetup.THING_ID);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright (c) 2021 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.ditto.things.model.signals.commands.exceptions;

import java.net.URI;

import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.annotation.concurrent.NotThreadSafe;

import org.eclipse.ditto.base.model.common.HttpStatus;
import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
import org.eclipse.ditto.base.model.exceptions.DittoRuntimeExceptionBuilder;
import org.eclipse.ditto.base.model.headers.DittoHeaders;
import org.eclipse.ditto.base.model.json.JsonParsableException;
import org.eclipse.ditto.json.JsonObject;
import org.eclipse.ditto.things.model.ThingException;

/**
* Thrown when the live channel condition was used for e.g. modify commands where it is not supported.
*
* @since 2.3.0
*/
@Immutable
@JsonParsableException(errorCode = LiveChannelConditionNotAllowedException.ERROR_CODE)
public final class LiveChannelConditionNotAllowedException extends DittoRuntimeException implements ThingException {

/**
* Error code of this exception.
*/
public static final String ERROR_CODE = ERROR_CODE_PREFIX + "live.channelcondition.notallowed";

private static final String DEFAULT_MESSAGE =
"The specified 'live-channel-condition' is only allowed to be used for retrieving API calls.";

private static final String DEFAULT_DESCRIPTION = "For modifying API calls the 'live-channel-condition' cannot " +
"be used, please remove the condition.";

private static final long serialVersionUID = 1239673920456383015L;

private LiveChannelConditionNotAllowedException(final DittoHeaders dittoHeaders,
@Nullable final String message,
@Nullable final String description,
@Nullable final Throwable cause,
@Nullable final URI href) {
super(ERROR_CODE, HttpStatus.METHOD_NOT_ALLOWED, dittoHeaders, message, description, cause, href);
}

/**
* A mutable builder for a {@link LiveChannelConditionNotAllowedException}.
*
* @return the builder.
*/
public static Builder newBuilder() {
return new Builder();
}

/**
* Constructs a new {@link LiveChannelConditionNotAllowedException}
* object with the exception message extracted from the given JSON object.
*
* @param jsonObject the JSON to read the {@link DittoRuntimeException.JsonFields#MESSAGE} field from.
* @param dittoHeaders the headers of the command which resulted in this exception.
* @return the new {@link LiveChannelConditionNotAllowedException}.
* @throws NullPointerException if any argument is {@code null}.
* @throws org.eclipse.ditto.json.JsonMissingFieldException if this JsonObject did not contain an error message.
* @throws org.eclipse.ditto.json.JsonParseException if the passed in {@code jsonObject} was not in the expected
* format.
*/
public static LiveChannelConditionNotAllowedException fromJson(final JsonObject jsonObject,
final DittoHeaders dittoHeaders) {
return DittoRuntimeException.fromJson(jsonObject, dittoHeaders, new Builder());
}

@Override
public DittoRuntimeException setDittoHeaders(final DittoHeaders dittoHeaders) {
return new Builder()
.message(getMessage())
.description(getDescription().orElse(null))
.cause(getCause())
.href(getHref().orElse(null))
.dittoHeaders(dittoHeaders)
.build();
}

/**
* A mutable builder with a fluent API for a {@link LiveChannelConditionNotAllowedException}.
*/
@NotThreadSafe
public static final class Builder
extends DittoRuntimeExceptionBuilder<LiveChannelConditionNotAllowedException> {

private Builder() {
this(DEFAULT_DESCRIPTION);
}

private Builder(final String description) {
message(DEFAULT_MESSAGE);
if (!description.equals("")) {
description(description);
}
}

@Override
protected LiveChannelConditionNotAllowedException doBuild(final DittoHeaders dittoHeaders,
@Nullable final String message,
@Nullable final String description,
@Nullable final Throwable cause,
@Nullable final URI href) {
return new LiveChannelConditionNotAllowedException(dittoHeaders, message, description, cause, href);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@

/**
* Thrown when validating the condition is failing.
*
* @since 2.2.0
*/
@Immutable
@JsonParsableException(errorCode = ThingConditionInvalidException.ERROR_CODE)
Expand All @@ -45,6 +47,8 @@ public final class ThingConditionInvalidException extends DittoRuntimeException
private static final String DEFAULT_DESCRIPTION = "The provided condition is not valid. " +
"Please check the value of your condition header.";

private static final long serialVersionUID = -973529278462389259L;

private ThingConditionInvalidException(final DittoHeaders dittoHeaders,
@Nullable final String message,
@Nullable final String description,
Expand All @@ -54,7 +58,7 @@ private ThingConditionInvalidException(final DittoHeaders dittoHeaders,
}

/**
* A mutable builder for a {@link org.eclipse.ditto.things.model.signals.commands.exceptions.ThingConditionInvalidException}.
* A mutable builder for a {@link ThingConditionInvalidException}.
*
* @param condition the condition to apply for the request.
* @return the builder.
Expand All @@ -64,12 +68,12 @@ public static Builder newBuilder(final String condition, final String descriptio
}

/**
* Constructs a new {@link org.eclipse.ditto.things.model.signals.commands.exceptions.ThingConditionInvalidException}
* Constructs a new {@link ThingConditionInvalidException}
* object with the exception message extracted from the given JSON object.
*
* @param jsonObject the JSON to read the {@link org.eclipse.ditto.base.model.exceptions.DittoRuntimeException.JsonFields#MESSAGE} field from.
* @param jsonObject the JSON to read the {@link DittoRuntimeException.JsonFields#MESSAGE} field from.
* @param dittoHeaders the headers of the command which resulted in this exception.
* @return the new {@link org.eclipse.ditto.things.model.signals.commands.exceptions.ThingConditionInvalidException}.
* @return the new {@link ThingConditionInvalidException}.
* @throws NullPointerException if any argument is {@code null}.
* @throws org.eclipse.ditto.json.JsonMissingFieldException if this JsonObject did not contain an error message.
* @throws org.eclipse.ditto.json.JsonParseException if the passed in {@code jsonObject} was not in the expected
Expand All @@ -92,7 +96,7 @@ public DittoRuntimeException setDittoHeaders(final DittoHeaders dittoHeaders) {
}

/**
* A mutable builder with a fluent API for a {@link org.eclipse.ditto.things.model.signals.commands.exceptions.ThingConditionInvalidException}.
* A mutable builder with a fluent API for a {@link ThingConditionInvalidException}.
*/
@NotThreadSafe
public static final class Builder
Expand Down

0 comments on commit ad1fefa

Please sign in to comment.