From 9c2ed96f3592e3157354d94095ecc09ff3685d56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20J=C3=A4ckle?= Date: Tue, 23 Jul 2024 11:57:12 +0200 Subject: [PATCH] #1650 added unit tests covering the added validation functionality --- things/service/src/main/resources/things.conf | 4 +- .../AbstractSingleDataSchemaBuilder.java | 7 + .../wot/model/MutablePropertyBuilder.java | 7 + .../ditto/wot/model/SingleDataSchema.java | 4 + .../validation/InternalFeatureValidation.java | 12 +- .../wot/validation/InternalValidation.java | 12 +- .../config/FeatureValidationConfig.java | 2 +- .../config/ThingValidationConfig.java | 2 +- ...tThingModelValidationFeatureLevelTest.java | 867 ++++++++++++++++++ ...WotThingModelValidationThingLevelTest.java | 540 +++++++++++ 10 files changed, 1447 insertions(+), 10 deletions(-) create mode 100644 wot/validation/src/test/java/org/eclipse/ditto/wot/validation/WotThingModelValidationFeatureLevelTest.java create mode 100644 wot/validation/src/test/java/org/eclipse/ditto/wot/validation/WotThingModelValidationThingLevelTest.java diff --git a/things/service/src/main/resources/things.conf b/things/service/src/main/resources/things.conf index 8fa2234437..b4a618552b 100755 --- a/things/service/src/main/resources/things.conf +++ b/things/service/src/main/resources/things.conf @@ -285,7 +285,7 @@ ditto { # whether to enforce a thing whenever the "definition" of the thing is updated to a new/other WoT TM # needed follow-up would likely be https://github.com/eclipse-ditto/ditto/issues/1843 # - only with that we would e.g. update all feature definitions to the ones of the new thing definitions - enforce-thing-description-modification = false # needs to be false until #1843 is done + enforce-thing-description-modification = true enforce-thing-description-modification = ${?THINGS_WOT_TM_MODEL_VALIDATION_THING_ENFORCE_TD_MODIFICATION} forbid-thing-description-deletion = true @@ -315,7 +315,7 @@ ditto { feature { # whether to enforce a feature whenever the "definition"(s) of the feature is updated to a new/other WoT TM(s) - enforce-feature-description-modification = false # needs to be false until #1843 is done + enforce-feature-description-modification = true enforce-feature-description-modification = ${?THINGS_WOT_TM_MODEL_VALIDATION_FEATURE_ENFORCE_FD_MODIFICATION} forbid-feature-description-deletion = true diff --git a/wot/model/src/main/java/org/eclipse/ditto/wot/model/AbstractSingleDataSchemaBuilder.java b/wot/model/src/main/java/org/eclipse/ditto/wot/model/AbstractSingleDataSchemaBuilder.java index d256f7ca6c..a4afdb9433 100644 --- a/wot/model/src/main/java/org/eclipse/ditto/wot/model/AbstractSingleDataSchemaBuilder.java +++ b/wot/model/src/main/java/org/eclipse/ditto/wot/model/AbstractSingleDataSchemaBuilder.java @@ -16,6 +16,7 @@ import java.util.Collection; import java.util.Optional; +import java.util.function.Consumer; import javax.annotation.Nullable; @@ -184,6 +185,12 @@ public B setType(@Nullable final DataSchemaType type) { return myself; } + @Override + public B enhanceObjectBuilder(final Consumer builderConsumer) { + builderConsumer.accept(wrappedObjectBuilder); + return myself; + } + protected void putValue(final JsonFieldDefinition definition, @Nullable final J value) { final Optional keyOpt = definition.getPointer().getRoot(); if (keyOpt.isPresent()) { diff --git a/wot/model/src/main/java/org/eclipse/ditto/wot/model/MutablePropertyBuilder.java b/wot/model/src/main/java/org/eclipse/ditto/wot/model/MutablePropertyBuilder.java index 68b313b794..fb3dea12c0 100644 --- a/wot/model/src/main/java/org/eclipse/ditto/wot/model/MutablePropertyBuilder.java +++ b/wot/model/src/main/java/org/eclipse/ditto/wot/model/MutablePropertyBuilder.java @@ -13,6 +13,7 @@ package org.eclipse.ditto.wot.model; import java.util.Collection; +import java.util.function.Consumer; import javax.annotation.Nullable; @@ -127,6 +128,12 @@ public Property.Builder setDefault(@Nullable final JsonValue defaultValue) { return myself; } + @Override + public Property.Builder enhanceObjectBuilder(final Consumer builderConsumer) { + builderConsumer.accept(wrappedObjectBuilder); + return myself; + } + @Override public Property.Builder setType(@Nullable final DataSchemaType type) { if (type != null) { diff --git a/wot/model/src/main/java/org/eclipse/ditto/wot/model/SingleDataSchema.java b/wot/model/src/main/java/org/eclipse/ditto/wot/model/SingleDataSchema.java index 514fd1c94d..8de9c2145f 100644 --- a/wot/model/src/main/java/org/eclipse/ditto/wot/model/SingleDataSchema.java +++ b/wot/model/src/main/java/org/eclipse/ditto/wot/model/SingleDataSchema.java @@ -16,6 +16,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; @@ -25,6 +26,7 @@ import org.eclipse.ditto.json.JsonFactory; import org.eclipse.ditto.json.JsonFieldDefinition; import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonObjectBuilder; import org.eclipse.ditto.json.JsonValue; /** @@ -157,6 +159,8 @@ interface Builder, S extends SingleDataSchema> { B setDefault(@Nullable JsonValue defaultValue); + B enhanceObjectBuilder(Consumer builderConsumer); + S build(); } diff --git a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/InternalFeatureValidation.java b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/InternalFeatureValidation.java index 0686c11553..cf5230e08e 100644 --- a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/InternalFeatureValidation.java +++ b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/InternalFeatureValidation.java @@ -266,7 +266,7 @@ private static CompletableFuture enforceFeaturePropertyOnlyDefinedProperti ) .toList()); final FeatureProperties featureProperties = FeatureProperties.newBuilder() - .setAll(propertyValue.asObject()) + .setAll(propertyValue.isObject() ? propertyValue.asObject() : JsonObject.empty()) .build(); return ensureOnlyDefinedProperties(featureThingModel, propertiesInCategory, @@ -305,6 +305,16 @@ private static CompletableFuture enforceFeaturePropertyValidateProperties( ) { if (isCategoryUpdate) { final String dittoCategory = propertyPath.getRoot().orElseThrow().toString(); + if (!propertyValue.isObject()) { + final WotThingModelPayloadValidationException.Builder exceptionBuilder = + WotThingModelPayloadValidationException + .newBuilder("Could not update Feature property category " + + "<" + dittoCategory + "> as its value was not a JSON object"); + return CompletableFuture.failedFuture(exceptionBuilder + .dittoHeaders(context.dittoHeaders()) + .build()); + } + final List sameCategoryProperties = tdProperties.values().stream() .filter(property -> // gather all properties from the same category diff --git a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/InternalValidation.java b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/InternalValidation.java index c86d953844..497bd6d0fa 100644 --- a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/InternalValidation.java +++ b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/InternalValidation.java @@ -46,6 +46,7 @@ final class InternalValidation { private static final JsonSchemaTools JSON_SCHEMA_TOOLS = new JsonSchemaTools(); + private static final String PROPERTIES_PATH_PREFIX = "/properties/"; private InternalValidation() { throw new AssertionError(); @@ -286,11 +287,11 @@ static Map extractRequiredTmProperties(final Properties tdProp final Map allRequiredProperties = new LinkedHashMap<>(tdProperties); tmOptionalElements.stream() .map(TmOptionalElement::toString) - .filter(el -> el.startsWith("/properties/")) - .map(el -> el.replace("/properties/", "")) + .filter(el -> el.startsWith(PROPERTIES_PATH_PREFIX)) + .map(el -> el.replace(PROPERTIES_PATH_PREFIX, "")) .forEach(allRequiredProperties::remove); return allRequiredProperties; - }).orElseGet(LinkedHashMap::new); + }).orElse(tdProperties); } static boolean isTmPropertyRequired(final Property property, @@ -299,8 +300,8 @@ static boolean isTmPropertyRequired(final Property property, return thingModel.getTmOptional() .map(tmOptionalElements -> tmOptionalElements.stream() .map(TmOptionalElement::toString) - .filter(el -> el.startsWith("/properties/")) - .map(el -> el.replace("/properties/", "")) + .filter(el -> el.startsWith(PROPERTIES_PATH_PREFIX)) + .map(el -> el.replace(PROPERTIES_PATH_PREFIX, "")) .noneMatch(el -> property.getPropertyName().equals(el)) ).orElse(false); } @@ -495,6 +496,7 @@ private static Optional findPropertyBasedOnPath(final Thin if (handleDittoCategory) { return dittoCategories.stream() .filter(category -> propertyPath.getRoot().orElseThrow().toString().equals(category)) + .filter(category -> propertyPath.getLevelCount() > 1) .map(category -> tdProperties.getProperty(propertyPath.get(1).orElseThrow().toString()) .filter(p -> determineDittoCategory(thingModel, p) diff --git a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/FeatureValidationConfig.java b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/FeatureValidationConfig.java index 5885cffd12..4bd6d94198 100644 --- a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/FeatureValidationConfig.java +++ b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/FeatureValidationConfig.java @@ -98,7 +98,7 @@ public interface FeatureValidationConfig { */ enum ConfigValue implements KnownConfigValue { - ENFORCE_FEATURE_DESCRIPTION_MODIFICATION("enforce-feature-description-modification", false), + ENFORCE_FEATURE_DESCRIPTION_MODIFICATION("enforce-feature-description-modification", true), FORBID_FEATURE_DESCRIPTION_DELETION("forbid-feature-description-deletion", true), diff --git a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/ThingValidationConfig.java b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/ThingValidationConfig.java index 139c3fdb30..7cb8ea067c 100644 --- a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/ThingValidationConfig.java +++ b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/ThingValidationConfig.java @@ -75,7 +75,7 @@ public interface ThingValidationConfig { */ enum ConfigValue implements KnownConfigValue { - ENFORCE_THING_DESCRIPTION_MODIFICATION("enforce-thing-description-modification", false), + ENFORCE_THING_DESCRIPTION_MODIFICATION("enforce-thing-description-modification", true), FORBID_THING_DESCRIPTION_DELETION("forbid-thing-description-deletion", true), diff --git a/wot/validation/src/test/java/org/eclipse/ditto/wot/validation/WotThingModelValidationFeatureLevelTest.java b/wot/validation/src/test/java/org/eclipse/ditto/wot/validation/WotThingModelValidationFeatureLevelTest.java new file mode 100644 index 0000000000..c9275709f7 --- /dev/null +++ b/wot/validation/src/test/java/org/eclipse/ditto/wot/validation/WotThingModelValidationFeatureLevelTest.java @@ -0,0 +1,867 @@ +/* + * Copyright (c) 2024 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.wot.validation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import javax.annotation.Nullable; + +import org.eclipse.ditto.base.model.headers.DittoHeaders; +import org.eclipse.ditto.json.JsonArray; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonPointer; +import org.eclipse.ditto.json.JsonValue; +import org.eclipse.ditto.things.model.Feature; +import org.eclipse.ditto.things.model.FeatureProperties; +import org.eclipse.ditto.things.model.Features; +import org.eclipse.ditto.wot.model.Action; +import org.eclipse.ditto.wot.model.Actions; +import org.eclipse.ditto.wot.model.AtContext; +import org.eclipse.ditto.wot.model.BaseLink; +import org.eclipse.ditto.wot.model.Event; +import org.eclipse.ditto.wot.model.Events; +import org.eclipse.ditto.wot.model.Links; +import org.eclipse.ditto.wot.model.Properties; +import org.eclipse.ditto.wot.model.Property; +import org.eclipse.ditto.wot.model.SingleAtContext; +import org.eclipse.ditto.wot.model.SingleDataSchema; +import org.eclipse.ditto.wot.model.SingleUriAtContext; +import org.eclipse.ditto.wot.model.ThingModel; +import org.eclipse.ditto.wot.model.TmOptional; +import org.eclipse.ditto.wot.model.TmOptionalElement; +import org.eclipse.ditto.wot.validation.config.FeatureValidationConfig; +import org.eclipse.ditto.wot.validation.config.TmValidationConfig; +import org.junit.Before; +import org.junit.Test; + +/** + * Provides unit tests for testing the "feature" related functionality of {@link WotThingModelValidation}. + */ +public final class WotThingModelValidationFeatureLevelTest { + + private static final String DITTO_CONTEXT_PREFIX = "ditto"; + private static final String CATEGORY_CONFIG = "config"; + + private static final String PROP_SOME_BOOL = "someBool"; + private static final String PROP_SOME_INT = "someInt"; + private static final JsonPointer PROP_PATH_SOME_INT = JsonPointer.of(CATEGORY_CONFIG + "/" + PROP_SOME_INT); + private static final String PROP_SOME_NUMBER = "someNumber"; + private static final String PROP_SOME_STRING = "someString"; + private static final JsonPointer PROP_PATH_SOME_STRING = JsonPointer.of(CATEGORY_CONFIG + "/" + PROP_SOME_STRING); + private static final String PROP_SOME_ARRAY_STRINGS = "someArray_strings"; + private static final String PROP_SOME_OBJECT = "someObject"; + + private static final JsonObject PROP_KNOWN_SOME_OBJECT = JsonObject.newBuilder() + .set(PROP_SOME_BOOL, false) + .set(PROP_SOME_INT, 3) + .set(PROP_SOME_STRING, "helo") + .build(); + + private static final Properties KNOWN_PROPERTIES = Properties.from(List.of( + Property.newBuilder(PROP_SOME_BOOL) + .setSchema(SingleDataSchema.newBooleanSchemaBuilder().build()) + .build(), + Property.newBuilder(PROP_SOME_INT) + .setSchema(SingleDataSchema.newIntegerSchemaBuilder().build()) + .set(DITTO_CONTEXT_PREFIX + ":category", CATEGORY_CONFIG) + .build(), + Property.newBuilder(PROP_SOME_NUMBER) + .setSchema(SingleDataSchema.newNumberSchemaBuilder().build()) + .build(), + Property.newBuilder(PROP_SOME_STRING) + .setSchema(SingleDataSchema.newStringSchemaBuilder().build()) + .set(DITTO_CONTEXT_PREFIX + ":category", CATEGORY_CONFIG) + .build(), + Property.newBuilder(PROP_SOME_ARRAY_STRINGS) + .setSchema(SingleDataSchema.newArraySchemaBuilder() + .setItems(SingleDataSchema.newStringSchemaBuilder().build()) + .build()) + .build(), + Property.newBuilder(PROP_SOME_OBJECT) + .setSchema(SingleDataSchema.newObjectSchemaBuilder() + .setProperties(Map.of( + PROP_SOME_BOOL, SingleDataSchema.newBooleanSchemaBuilder().build(), + PROP_SOME_INT, SingleDataSchema.newIntegerSchemaBuilder().build(), + PROP_SOME_STRING, SingleDataSchema.newStringSchemaBuilder().build() + )) + .setRequired(List.of(PROP_SOME_BOOL, PROP_SOME_STRING)) + .enhanceObjectBuilder(builder -> builder.set("additionalProperties", false)) + .build()) + .build() + )); + + private static final String ACTION_PROCESS_BOOL = "processBool"; + private static final String ACTION_PROCESS_OBJECT = "processObject"; + + private static final Actions KNOWN_ACTIONS = Actions.from(List.of( + Action.newBuilder(ACTION_PROCESS_BOOL) + .setInput(SingleDataSchema.newBooleanSchemaBuilder().build()) + .setOutput(SingleDataSchema.newBooleanSchemaBuilder().build()) + .build(), + Action.newBuilder(ACTION_PROCESS_OBJECT) + .setInput(SingleDataSchema.newObjectSchemaBuilder() + .setProperties(Map.of( + PROP_SOME_BOOL, SingleDataSchema.newBooleanSchemaBuilder().build(), + PROP_SOME_INT, SingleDataSchema.newIntegerSchemaBuilder().build(), + PROP_SOME_STRING, SingleDataSchema.newStringSchemaBuilder().build() + )) + .setRequired(List.of(PROP_SOME_BOOL, PROP_SOME_STRING)) + .build() + ) + .setOutput(SingleDataSchema.newObjectSchemaBuilder() + .setProperties(Map.of( + PROP_SOME_BOOL, SingleDataSchema.newBooleanSchemaBuilder().build(), + PROP_SOME_INT, SingleDataSchema.newIntegerSchemaBuilder().build(), + PROP_SOME_STRING, SingleDataSchema.newStringSchemaBuilder().build() + )) + .setRequired(List.of(PROP_SOME_BOOL, PROP_SOME_STRING)) + .build()) + .build() + )); + + private static final String EVENT_EMIT_INT = "emitInt"; + private static final String EVENT_EMIT_ARRAY = "emitArray_objects"; + + private static final Events KNOWN_EVENTS = Events.from(List.of( + Event.newBuilder(EVENT_EMIT_INT) + .setData(SingleDataSchema.newIntegerSchemaBuilder().build()) + .build(), + Event.newBuilder(EVENT_EMIT_ARRAY) + .setData(SingleDataSchema.newArraySchemaBuilder() + .setItems(SingleDataSchema.newObjectSchemaBuilder() + .setProperties(Map.of( + PROP_SOME_BOOL, SingleDataSchema.newBooleanSchemaBuilder().build(), + PROP_SOME_INT, SingleDataSchema.newIntegerSchemaBuilder().build(), + PROP_SOME_STRING, SingleDataSchema.newStringSchemaBuilder().build() + )) + .setRequired(List.of(PROP_SOME_BOOL, PROP_SOME_STRING)) + .build()) + .build() + ) + .build() + )); + + private static final String KNOWN_FEATURE_ID = "known-feature"; + private static final String KNOWN_FEATURE_ID_2 = "known-feature-2"; + + private static final ThingModel KNOWN_THING_LEVEL_TM_WITH_SUBMODELS = ThingModel.newBuilder() + .setLinks(Links.of(List.of( + BaseLink.newLinkBuilder() + .setType("tm:submodel") + .build() + ))) + .build(); + + private static final ThingModel KNOWN_FEATURE_LEVEL_TM = ThingModel.newBuilder() + .setAtContext(AtContext.newMultipleAtContext(List.of( + SingleAtContext.newSinglePrefixedAtContext(DITTO_CONTEXT_PREFIX, + SingleUriAtContext.DITTO_WOT_EXTENSION) + ))) + .setProperties(KNOWN_PROPERTIES) + .setTmOptional(TmOptional.of(List.of( + TmOptionalElement.of("/properties/" + PROP_SOME_ARRAY_STRINGS), + TmOptionalElement.of("/properties/" + PROP_SOME_OBJECT) + ))) + .setActions(KNOWN_ACTIONS) + .setEvents(KNOWN_EVENTS) + .build(); + + private static final FeatureProperties KNOWN_FEATURE_PROPERTIES = FeatureProperties.newBuilder() + .set(PROP_SOME_BOOL, true) + .set(PROP_PATH_SOME_INT, 42) + .set(PROP_SOME_NUMBER, 42.23) + .set(PROP_PATH_SOME_STRING, "some") + .build(); + + + private WotThingModelValidation sut; + + @Before + public void setUp() { + final TmValidationConfig validationConfig = mock(TmValidationConfig.class); + when(validationConfig.isEnabled()).thenReturn(true); + + final FeatureValidationConfig featureValidationConfig = mock(FeatureValidationConfig.class); + when(featureValidationConfig.isEnforcePresenceOfModeledFeatures()).thenReturn(true); + when(featureValidationConfig.isForbidNonModeledFeatures()).thenReturn(true); + when(featureValidationConfig.isEnforceProperties()).thenReturn(true); + when(featureValidationConfig.isEnforceDesiredProperties()).thenReturn(true); + when(featureValidationConfig.isForbidNonModeledProperties()).thenReturn(true); + when(featureValidationConfig.isForbidNonModeledDesiredProperties()).thenReturn(true); + when(featureValidationConfig.isEnforceInboxMessagesInput()).thenReturn(true); + when(featureValidationConfig.isEnforceInboxMessagesOutput()).thenReturn(true); + when(featureValidationConfig.isEnforceOutboxMessages()).thenReturn(true); + when(featureValidationConfig.isForbidNonModeledInboxMessages()).thenReturn(true); + when(featureValidationConfig.isForbidNonModeledOutboxMessages()).thenReturn(true); + when(validationConfig.getFeatureValidationConfig()).thenReturn(featureValidationConfig); + + sut = WotThingModelValidation.of(validationConfig); + } + + @Test + public void validateFeaturesDeletionSucceedsWithThingLevelModelNotHavingSubmodels() { + internalCheckFail(false, sut.validateThingScopedDeletion(ThingModel.newBuilder().build(), + Map.of(), + JsonPointer.of("features"), + provideValidationContext() + )); + } + + @Test + public void validateFeaturesDeletionFailsWithThingLevelModelHavingSubmodels() { + internalCheckFail(true, sut.validateThingScopedDeletion(KNOWN_THING_LEVEL_TM_WITH_SUBMODELS, + Map.of(KNOWN_FEATURE_ID, KNOWN_FEATURE_LEVEL_TM), + JsonPointer.of("features"), + provideValidationContext() + )); + } + + @Test + public void validateFeaturesPresenceSucceedsWhenAllModeledFeaturesArePresent() { + checkValidateFeaturesPresence(Features.newBuilder() + .set(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .withId(KNOWN_FEATURE_ID) + .build()) + .set(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .withId(KNOWN_FEATURE_ID_2) + .build()) + .build(), + false + ); + } + + @Test + public void validateFeaturesPresenceFailsWhenNotAllModeledFeaturesArePresent() { + checkValidateFeaturesPresence(Features.newBuilder() + .set(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .withId(KNOWN_FEATURE_ID) + .build()) + .build(), + true + ); + } + + @Test + public void validateFeaturesPresenceFailsWhenNonModeledFeaturesAreProvided() { + checkValidateFeaturesPresence(Features.newBuilder() + .set(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .withId(KNOWN_FEATURE_ID) + .build()) + .set(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .withId(KNOWN_FEATURE_ID_2) + .build()) + .set(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .withId("unknown-feature") + .build()) + .build(), + true + ); + } + + @Test + public void validateFeaturesPropertiesSucceeds() { + checkValidateFeaturesProperties(Features.newBuilder() + .set(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .withId(KNOWN_FEATURE_ID) + .build()) + .set(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .withId(KNOWN_FEATURE_ID_2) + .build()) + .build(), + false + ); + } + + @Test + public void validateFeaturesPropertiesFailsWhenMissingRequiredProperty() { + checkValidateFeaturesProperties(Features.newBuilder() + .set(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES.toBuilder().remove(PROP_SOME_BOOL).build()) + .withId(KNOWN_FEATURE_ID) + .build()) + .set(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .withId(KNOWN_FEATURE_ID_2) + .build()) + .build(), + true + ); + } + + @Test + public void validateFeaturesPropertiesFailsWhenPropertyHasWrongDatatype() { + checkValidateFeaturesProperties(Features.newBuilder() + .set(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES.toBuilder().set(PROP_SOME_BOOL, "not a bool").build()) + .withId(KNOWN_FEATURE_ID) + .build()) + .set(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .withId(KNOWN_FEATURE_ID_2) + .build()) + .build(), + true + ); + } + + @Test + public void validateFeaturePresenceSucceeds() { + checkValidateFeaturePresence(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .withId(KNOWN_FEATURE_ID) + .build(), + false + ); + } + + @Test + public void validateFeaturePresenceFails() { + checkValidateFeaturePresence(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .withId("unknown-id") + .build(), + true + ); + } + + @Test + public void validateFeatureSucceeds() { + checkValidateFeature(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .withId(KNOWN_FEATURE_ID) + .build(), + false + ); + } + + @Test + public void validateFeatureSucceedsWithOptionalDesiredPropertyPresent() { + checkValidateFeature(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .desiredProperties(FeatureProperties.newBuilder() + .set(PROP_PATH_SOME_INT, 42) + .build() + ) + .withId(KNOWN_FEATURE_ID) + .build(), + false + ); + } + + @Test + public void validateFeatureFailsWhenMissingRequiredProperty() { + checkValidateFeature(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES.toBuilder().remove(PROP_PATH_SOME_STRING).build()) + .withId(KNOWN_FEATURE_ID) + .build(), + true + ); + } + + @Test + public void validateFeatureFailsWithOptionalDesiredPropertyHavingWrongDatatype() { + checkValidateFeature(Feature.newBuilder() + .properties(KNOWN_FEATURE_PROPERTIES) + .desiredProperties(FeatureProperties.newBuilder() + .set(PROP_PATH_SOME_INT, "not an int") + .build() + ) + .withId(KNOWN_FEATURE_ID) + .build(), + true + ); + } + + @Test + public void validateFeaturePropertySucceedsForBooleanProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_BOOL, false, JsonValue.of(true), false); + } + + @Test + public void validateFeaturePropertiesSucceedsForBooleanProperty() { + checkValidateFeatureProperties(PROP_SOME_BOOL, false, JsonValue.of(true), false); + } + + @Test + public void validateFeaturePropertyFailsForBooleanProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_BOOL, false, JsonValue.of("something else"), true); + } + + @Test + public void validateFeaturePropertiesFailsForBooleanProperty() { + checkValidateFeatureProperties(PROP_SOME_BOOL, false, JsonValue.of("something else"), true); + } + + @Test + public void validateFeaturePropertySucceedsForIntegerProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_PATH_SOME_INT, false, JsonValue.of(42), false); + } + + @Test + public void validateFeaturePropertiesSucceedsForIntegerProperty() { + checkValidateFeatureProperties(PROP_PATH_SOME_INT, false, JsonValue.of(42), false); + } + + @Test + public void validateFeaturePropertyFailsForIntegerProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_PATH_SOME_INT, false, JsonValue.of("something else"), true); + } + + @Test + public void validateFeaturePropertiesFailsForIntegerProperty() { + checkValidateFeatureProperties(PROP_PATH_SOME_INT, false, JsonValue.of("something else"), true); + } + + @Test + public void validateFeaturePropertySucceedsForNumberProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_NUMBER, false, JsonValue.of(42.23), false); + } + + @Test + public void validateFeaturePropertiesSucceedsForNumberProperty() { + checkValidateFeatureProperties(PROP_SOME_NUMBER, false, JsonValue.of(42.23), false); + } + + @Test + public void validateFeaturePropertyFailsForNumberProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_NUMBER, false, JsonValue.of("something else"), true); + } + + @Test + public void validateFeaturePropertiesFailsForNumberProperty() { + checkValidateFeatureProperties(PROP_SOME_NUMBER, false, JsonValue.of("something else"), true); + } + + @Test + public void validateFeaturePropertySucceedsForStringProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_PATH_SOME_STRING, false, JsonValue.of("some"), false); + } + + @Test + public void validateFeaturePropertiesSucceedsForStringProperty() { + checkValidateFeatureProperties(PROP_PATH_SOME_STRING, false, JsonValue.of("some"), false); + } + + @Test + public void validateFeaturePropertyFailsForStringProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_PATH_SOME_STRING, false, JsonValue.of(false), true); + } + + @Test + public void validateFeaturePropertiesFailsForStringProperty() { + checkValidateFeatureProperties(PROP_PATH_SOME_STRING, false, JsonValue.of(false), true); + } + + @Test + public void validateFeaturePropertySucceedsForArraysStringProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_ARRAY_STRINGS, false, + JsonArray.of("some", "string", "arr"), + false); + } + + @Test + public void validateFeaturePropertiesSucceedsForArraysStringProperty() { + checkValidateFeatureProperties(PROP_SOME_ARRAY_STRINGS, false, JsonArray.of("some", "string", "arr"), false); + } + + @Test + public void validateFeaturePropertyFailsForArraysStringProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_ARRAY_STRINGS, false, JsonArray.of(false, true, false), + true); + } + + @Test + public void validateFeaturePropertiesFailsForArraysStringProperty() { + checkValidateFeatureProperties(PROP_SOME_ARRAY_STRINGS, false, JsonArray.of(false, true, false), true); + } + + @Test + public void validateFeaturePropertySucceedsForObjectProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_OBJECT, false, PROP_KNOWN_SOME_OBJECT, false); + } + + @Test + public void validateFeaturePropertySucceedsForCategoryUpdateContainingAllRequiredProperties() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, CATEGORY_CONFIG, false, JsonObject.newBuilder() + .set(PROP_SOME_INT, 44) + .set(PROP_SOME_STRING, "some") + .build(), + false + ); + } + + @Test + public void validateFeaturePropertyFailsForCategoryUpdateMissingRequiredProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, CATEGORY_CONFIG, false, JsonObject.newBuilder() + .set(PROP_SOME_INT, 44) + .build(), + true + ); + } + + @Test + public void validateFeaturePropertyFailsForCategoryUpdateNotBeingObject() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, CATEGORY_CONFIG, false, JsonValue.of("not a category object"), + true + ); + } + + @Test + public void validateFeaturePropertiesSucceedsForObjectProperty() { + checkValidateFeatureProperties(PROP_SOME_OBJECT, false, PROP_KNOWN_SOME_OBJECT, false); + } + + @Test + public void validateFeaturePropertyFailsForObjectProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_OBJECT, false, JsonValue.of(false), true); + } + + @Test + public void validateFeaturePropertiesFailsForObjectProperty() { + checkValidateFeatureProperties(PROP_SOME_OBJECT, false, JsonValue.of(false), true); + } + + @Test + public void validateFeaturePropertyFailsForObjectAttributeMissingRequiredField() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_ARRAY_STRINGS, false, + PROP_KNOWN_SOME_OBJECT.toBuilder().remove(PROP_PATH_SOME_STRING).build(), true); + } + + @Test + public void validateFeaturePropertiesFailsForObjectAttributeMissingRequiredField() { + checkValidateFeatureProperties(PROP_SOME_ARRAY_STRINGS, false, + PROP_KNOWN_SOME_OBJECT.toBuilder().remove(PROP_PATH_SOME_STRING).build(), true); + } + + @Test + public void validateFeaturePropertyFailsForObjectAttributeAdditionalNotSpecifiedField() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_ARRAY_STRINGS, false, + PROP_KNOWN_SOME_OBJECT.toBuilder().set("new", "foo").build(), true); + } + + @Test + public void validateFeaturePropertiesFailsForObjectAttributeAdditionalNotSpecifiedField() { + checkValidateFeatureProperties(PROP_SOME_ARRAY_STRINGS, false, + PROP_KNOWN_SOME_OBJECT.toBuilder().set("new", "foo").build(), true); + } + + @Test + public void validateFeaturePropertyFailsForNonModeledProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, "newStuff", false, JsonValue.of(true), true); + } + + @Test + public void validateFeaturePropertiesFailsForNonModeledProperties() { + checkValidateFeatureProperties("newStuff", false, JsonValue.of(true), true); + } + + @Test + public void validateFeaturePropertyFailsForNonModeledDesiredProperty() { + checkValidateFeatureProperty(KNOWN_FEATURE_ID, "newStuff", true, JsonValue.of(true), true); + } + + @Test + public void validateFeaturePropertiesFailsForNonModeledDesiredProperties() { + checkValidateFeatureProperties("newStuff", true, JsonValue.of(true), true); + } + + @Test + public void validateFeaturePropertyDeletionSucceedsForOptionalArrayProperty() { + checkValidateFeaturePropertyDeletion( + JsonPointer.of("features/" + KNOWN_FEATURE_ID + "/properties/" + PROP_SOME_ARRAY_STRINGS), + false + ); + } + + @Test + public void validateFeaturePropertyDeletionFailsForFeatureContainingRequiredProperties() { + checkValidateFeaturePropertyDeletion( + JsonPointer.of("features/" + KNOWN_FEATURE_ID), + true + ); + } + + @Test + public void validateFeaturePropertyDeletionFailsForFeaturePropertiesContainingRequiredProperties() { + checkValidateFeaturePropertyDeletion( + JsonPointer.of("features/" + KNOWN_FEATURE_ID + "/properties/"), + true + ); + } + + @Test + public void validateFeaturePropertyDeletionFailsForFeaturePropertiesCategoryContainingRequiredProperties() { + checkValidateFeaturePropertyDeletion( + JsonPointer.of("features/" + KNOWN_FEATURE_ID + "/properties/" + CATEGORY_CONFIG), + true + ); + } + + @Test + public void validateFeatureActionBoolInputSucceeds() { + checkValidateFeatureActionInput(KNOWN_FEATURE_ID, ACTION_PROCESS_BOOL, JsonValue.of(true), false); + } + + @Test + public void validateFeatureActionBoolInputFailsWrongDatatype() { + checkValidateFeatureActionInput(KNOWN_FEATURE_ID, ACTION_PROCESS_BOOL, JsonValue.of("oh no"), true); + } + + @Test + public void validateFeatureActionObjectInputSucceeds() { + checkValidateFeatureActionInput(KNOWN_FEATURE_ID, ACTION_PROCESS_OBJECT, JsonObject.newBuilder() + .set(PROP_SOME_BOOL, false) + .set(PROP_SOME_INT, 23) + .set(PROP_SOME_STRING, "yes!") + .build(), false); + } + + @Test + public void validateFeatureActionObjectInputFailsWrongDatatype() { + checkValidateFeatureActionInput(KNOWN_FEATURE_ID, ACTION_PROCESS_OBJECT, JsonArray.empty(), true); + } + + @Test + public void validateFeatureActionObjectInputFailsMissingRequiredField() { + checkValidateFeatureActionInput(KNOWN_FEATURE_ID, ACTION_PROCESS_OBJECT, JsonObject.newBuilder() + .set(PROP_PATH_SOME_STRING, "yes!") + .build(), true); + } + + @Test + public void validateNonModeledFeatureActionInputFails() { + checkValidateFeatureActionInput(KNOWN_FEATURE_ID, "someNewAction", JsonValue.of(true), true); + } + + @Test + public void validateFeatureActionBoolOutputSucceeds() { + checkValidateFeatureActionOutput(KNOWN_FEATURE_ID, ACTION_PROCESS_BOOL, JsonValue.of(true), false); + } + + @Test + public void validateFeatureActionBoolOutputFailsWrongDatatype() { + checkValidateFeatureActionOutput(KNOWN_FEATURE_ID, ACTION_PROCESS_BOOL, JsonValue.of("oh no"), true); + } + + @Test + public void validateFeatureActionObjectOutputSucceeds() { + checkValidateFeatureActionOutput(KNOWN_FEATURE_ID, ACTION_PROCESS_OBJECT, JsonObject.newBuilder() + .set(PROP_SOME_BOOL, false) + .set(PROP_SOME_INT, 23) + .set(PROP_SOME_STRING, "yes!") + .build(), false); + } + + @Test + public void validateFeatureActionObjectOutputFailsWrongDatatype() { + checkValidateFeatureActionOutput(KNOWN_FEATURE_ID, ACTION_PROCESS_OBJECT, JsonArray.empty(), true); + } + + @Test + public void validateFeatureActionObjectOutputFailsMissingRequiredField() { + checkValidateFeatureActionOutput(KNOWN_FEATURE_ID, ACTION_PROCESS_OBJECT, JsonObject.newBuilder() + .set(PROP_PATH_SOME_STRING, "yes!") + .build(), true); + } + + @Test + public void validateFeatureEventIntDataSucceeds() { + checkValidateFeatureEventData(KNOWN_FEATURE_ID, EVENT_EMIT_INT, JsonValue.of(33), false); + } + + @Test + public void validateFeatureEventBoolDataFailsWrongDatatype() { + checkValidateFeatureEventData(KNOWN_FEATURE_ID, EVENT_EMIT_INT, JsonValue.of("oh no"), true); + } + + @Test + public void validateFeatureEventArrayDataSucceeds() { + checkValidateFeatureEventData(KNOWN_FEATURE_ID, EVENT_EMIT_ARRAY, JsonArray.newBuilder() + .add(JsonObject.newBuilder() + .set(PROP_SOME_BOOL, false) + .set(PROP_SOME_INT, 23) + .set(PROP_SOME_STRING, "yes!") + .build() + ).build(), false); + } + + @Test + public void validateFeatureEventArrayDataFailsWrongDatatype() { + checkValidateFeatureEventData(KNOWN_FEATURE_ID, EVENT_EMIT_ARRAY, JsonObject.empty(), true); + } + + @Test + public void validateFeatureEventArrayDataFailsMissingRequiredFieldInsideObject() { + checkValidateFeatureEventData(KNOWN_FEATURE_ID, EVENT_EMIT_ARRAY, JsonArray.newBuilder() + .add(JsonObject.newBuilder() + .set(PROP_PATH_SOME_INT, 23) + .set(PROP_PATH_SOME_STRING, "yes!") + .build() + ).build(), true); + } + + @Test + public void validateNonModeledFeatureEventDataFails() { + checkValidateFeatureEventData(KNOWN_FEATURE_ID, "someNewEvent", JsonValue.of(true), true); + } + + private void checkValidateFeaturesPresence(@Nullable final Features features, final boolean mustFail) { + internalCheckFail(mustFail, sut.validateFeaturesPresence(Map.of( + KNOWN_FEATURE_ID, KNOWN_FEATURE_LEVEL_TM, + KNOWN_FEATURE_ID_2, KNOWN_FEATURE_LEVEL_TM + ), + features, + provideValidationContext() + )); + } + + private void checkValidateFeaturesProperties(@Nullable final Features features, final boolean mustFail) { + internalCheckFail(mustFail, sut.validateFeaturesProperties(Map.of( + KNOWN_FEATURE_ID, KNOWN_FEATURE_LEVEL_TM, + KNOWN_FEATURE_ID_2, KNOWN_FEATURE_LEVEL_TM + ), + features, + JsonPointer.of("features"), + provideValidationContext() + )); + } + + private void checkValidateFeaturePresence(final Feature feature, final boolean mustFail) { + internalCheckFail(mustFail, sut.validateFeaturePresence(Map.of( + KNOWN_FEATURE_ID, KNOWN_FEATURE_LEVEL_TM, + KNOWN_FEATURE_ID_2, KNOWN_FEATURE_LEVEL_TM + ), + feature, + provideValidationContext() + )); + } + + private void checkValidateFeature(final Feature feature, final boolean mustFail) { + internalCheckFail(mustFail, sut.validateFeature(KNOWN_FEATURE_LEVEL_TM, + feature, + JsonPointer.of("features/" + feature.getId()), + provideValidationContext() + )); + } + + private void checkValidateFeatureProperty(final String featureId, final CharSequence propertyPath, + final boolean desiredProperty, + final JsonValue propertyValue, + final boolean mustFail + ) { + final JsonPointer propertyPointer = JsonPointer.of(propertyPath); + internalCheckFail(mustFail, sut.validateFeatureProperty(KNOWN_FEATURE_LEVEL_TM, + KNOWN_FEATURE_ID, + propertyPointer, + propertyValue, + desiredProperty, + JsonPointer.of("features/" + featureId + "/properties").append(propertyPointer), + provideValidationContext() + )); + } + + private void checkValidateFeatureProperties(final CharSequence propertyPath, final boolean desiredProperties, + final JsonValue propertyValue, + final boolean mustFail + ) { + internalCheckFail(mustFail, sut.validateFeatureProperties(KNOWN_FEATURE_LEVEL_TM, + KNOWN_FEATURE_ID, + KNOWN_FEATURE_PROPERTIES.toBuilder() + .set(propertyPath, propertyValue) + .build(), + desiredProperties, + JsonPointer.of("attributes"), + provideValidationContext() + )); + } + + private void checkValidateFeaturePropertyDeletion(final JsonPointer resourcePath, final boolean mustFail) { + internalCheckFail(mustFail, sut.validateFeatureScopedDeletion(Map.of(KNOWN_FEATURE_ID, KNOWN_FEATURE_LEVEL_TM), + KNOWN_FEATURE_LEVEL_TM, + KNOWN_FEATURE_ID, + resourcePath, + provideValidationContext() + )); + } + + private void checkValidateFeatureActionInput(final String featureId, final String actionName, + @Nullable final JsonValue inputData, + final boolean mustFail + ) { + internalCheckFail(mustFail, sut.validateFeatureActionInput(KNOWN_FEATURE_LEVEL_TM, + featureId, + actionName, + inputData, + JsonPointer.of("features/" + featureId + "/inbox/messages/" + actionName), + provideValidationContext() + )); + } + + private void checkValidateFeatureActionOutput(final String featureId, final String actionName, + @Nullable final JsonValue outputData, + final boolean mustFail + ) { + internalCheckFail(mustFail, sut.validateFeatureActionOutput(KNOWN_FEATURE_LEVEL_TM, + featureId, + actionName, + outputData, + JsonPointer.of("features/" + featureId + "/inbox/messages/" + actionName), + provideValidationContext() + )); + } + + private void checkValidateFeatureEventData(final String featureId, final String eventName, + @Nullable final JsonValue data, + final boolean mustFail + ) { + internalCheckFail(mustFail, sut.validateFeatureEventData(KNOWN_FEATURE_LEVEL_TM, + featureId, + eventName, + data, + JsonPointer.of("features/" + featureId + "/outbox/messages/" + eventName), + provideValidationContext() + )); + } + + private static ValidationContext provideValidationContext() { + return ValidationContext.buildValidationContext(DittoHeaders.empty(), null, null); + } + + private static void internalCheckFail(final boolean mustFail, final CompletionStage stage) { + if (mustFail) { + assertThat(stage) + .isCompletedExceptionally() + .failsWithin(50, TimeUnit.MILLISECONDS) + .withThrowableOfType(ExecutionException.class) + .withCauseInstanceOf(WotThingModelPayloadValidationException.class); + } else { + assertThat(stage).isNotCompletedExceptionally().isDone(); + } + } +} \ No newline at end of file diff --git a/wot/validation/src/test/java/org/eclipse/ditto/wot/validation/WotThingModelValidationThingLevelTest.java b/wot/validation/src/test/java/org/eclipse/ditto/wot/validation/WotThingModelValidationThingLevelTest.java new file mode 100644 index 0000000000..b9ad26d1dc --- /dev/null +++ b/wot/validation/src/test/java/org/eclipse/ditto/wot/validation/WotThingModelValidationThingLevelTest.java @@ -0,0 +1,540 @@ +/* + * Copyright (c) 2024 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.wot.validation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import javax.annotation.Nullable; + +import org.eclipse.ditto.base.model.headers.DittoHeaders; +import org.eclipse.ditto.json.JsonArray; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonPointer; +import org.eclipse.ditto.json.JsonValue; +import org.eclipse.ditto.things.model.Attributes; +import org.eclipse.ditto.wot.model.Action; +import org.eclipse.ditto.wot.model.Actions; +import org.eclipse.ditto.wot.model.AtContext; +import org.eclipse.ditto.wot.model.Event; +import org.eclipse.ditto.wot.model.Events; +import org.eclipse.ditto.wot.model.Properties; +import org.eclipse.ditto.wot.model.Property; +import org.eclipse.ditto.wot.model.SingleDataSchema; +import org.eclipse.ditto.wot.model.ThingModel; +import org.eclipse.ditto.wot.model.TmOptional; +import org.eclipse.ditto.wot.model.TmOptionalElement; +import org.eclipse.ditto.wot.validation.config.ThingValidationConfig; +import org.eclipse.ditto.wot.validation.config.TmValidationConfig; +import org.junit.Before; +import org.junit.Test; + +/** + * Provides unit tests for testing the "thing" related functionality of {@link WotThingModelValidation}. + */ +public final class WotThingModelValidationThingLevelTest { + + private static final String PROP_SOME_BOOL = "someBool"; + private static final String PROP_SOME_INT = "someInt"; + private static final String PROP_SOME_NUMBER = "someNumber"; + private static final String PROP_SOME_STRING = "someString"; + private static final String PROP_SOME_ARRAY_STRINGS = "someArray_strings"; + private static final String PROP_SOME_OBJECT = "someObject"; + + private static final JsonObject PROP_KNOWN_SOME_OBJECT = JsonObject.newBuilder() + .set(PROP_SOME_BOOL, false) + .set(PROP_SOME_INT, 3) + .set(PROP_SOME_STRING, "helo") + .build(); + + private static final Properties KNOWN_PROPERTIES = Properties.from(List.of( + Property.newBuilder(PROP_SOME_BOOL) + .setSchema(SingleDataSchema.newBooleanSchemaBuilder().build()) + .build(), + Property.newBuilder(PROP_SOME_INT) + .setSchema(SingleDataSchema.newIntegerSchemaBuilder().build()) + .build(), + Property.newBuilder(PROP_SOME_NUMBER) + .setSchema(SingleDataSchema.newNumberSchemaBuilder().build()) + .build(), + Property.newBuilder(PROP_SOME_STRING) + .setSchema(SingleDataSchema.newStringSchemaBuilder().build()) + .build(), + Property.newBuilder(PROP_SOME_ARRAY_STRINGS) + .setSchema(SingleDataSchema.newArraySchemaBuilder() + .setItems(SingleDataSchema.newStringSchemaBuilder().build()) + .build()) + .build(), + Property.newBuilder(PROP_SOME_OBJECT) + .setSchema(SingleDataSchema.newObjectSchemaBuilder() + .setProperties(Map.of( + PROP_SOME_BOOL, SingleDataSchema.newBooleanSchemaBuilder().build(), + PROP_SOME_INT, SingleDataSchema.newIntegerSchemaBuilder().build(), + PROP_SOME_STRING, SingleDataSchema.newStringSchemaBuilder().build() + )) + .setRequired(List.of(PROP_SOME_BOOL, PROP_SOME_STRING)) + .enhanceObjectBuilder(builder -> builder.set("additionalProperties", false)) + .build()) + .build() + )); + + private static final String ACTION_PROCESS_BOOL = "processBool"; + private static final String ACTION_PROCESS_OBJECT = "processObject"; + + private static final Actions KNOWN_ACTIONS = Actions.from(List.of( + Action.newBuilder(ACTION_PROCESS_BOOL) + .setInput(SingleDataSchema.newBooleanSchemaBuilder().build()) + .setOutput(SingleDataSchema.newBooleanSchemaBuilder().build()) + .build(), + Action.newBuilder(ACTION_PROCESS_OBJECT) + .setInput(SingleDataSchema.newObjectSchemaBuilder() + .setProperties(Map.of( + PROP_SOME_BOOL, SingleDataSchema.newBooleanSchemaBuilder().build(), + PROP_SOME_INT, SingleDataSchema.newIntegerSchemaBuilder().build(), + PROP_SOME_STRING, SingleDataSchema.newStringSchemaBuilder().build() + )) + .setRequired(List.of(PROP_SOME_BOOL, PROP_SOME_STRING)) + .build() + ) + .setOutput(SingleDataSchema.newObjectSchemaBuilder() + .setProperties(Map.of( + PROP_SOME_BOOL, SingleDataSchema.newBooleanSchemaBuilder().build(), + PROP_SOME_INT, SingleDataSchema.newIntegerSchemaBuilder().build(), + PROP_SOME_STRING, SingleDataSchema.newStringSchemaBuilder().build() + )) + .setRequired(List.of(PROP_SOME_BOOL, PROP_SOME_STRING)) + .build()) + .build() + )); + + private static final String EVENT_EMIT_INT = "emitInt"; + private static final String EVENT_EMIT_ARRAY = "emitArray_objects"; + + private static final Events KNOWN_EVENTS = Events.from(List.of( + Event.newBuilder(EVENT_EMIT_INT) + .setData(SingleDataSchema.newIntegerSchemaBuilder().build()) + .build(), + Event.newBuilder(EVENT_EMIT_ARRAY) + .setData(SingleDataSchema.newArraySchemaBuilder() + .setItems(SingleDataSchema.newObjectSchemaBuilder() + .setProperties(Map.of( + PROP_SOME_BOOL, SingleDataSchema.newBooleanSchemaBuilder().build(), + PROP_SOME_INT, SingleDataSchema.newIntegerSchemaBuilder().build(), + PROP_SOME_STRING, SingleDataSchema.newStringSchemaBuilder().build() + )) + .setRequired(List.of(PROP_SOME_BOOL, PROP_SOME_STRING)) + .build()) + .build() + ) + .build() + )); + + private static final ThingModel KNOWN_THING_LEVEL_TM = ThingModel.newBuilder() + .setAtContext(AtContext.newSingleUriAtContext("foo")) + .setProperties(KNOWN_PROPERTIES) + .setTmOptional(TmOptional.of(List.of( + TmOptionalElement.of("/properties/" + PROP_SOME_ARRAY_STRINGS), + TmOptionalElement.of("/properties/" + PROP_SOME_OBJECT) + ))) + .setActions(KNOWN_ACTIONS) + .setEvents(KNOWN_EVENTS) + .build(); + + private static final Attributes KNOWN_THING_ATTRIBUTES = Attributes.newBuilder() + .set(PROP_SOME_BOOL, true) + .set(PROP_SOME_INT, 42) + .set(PROP_SOME_NUMBER, 42.23) + .set(PROP_SOME_STRING, "some") + .build(); + + + private WotThingModelValidation sut; + + @Before + public void setUp() { + final TmValidationConfig validationConfig = mock(TmValidationConfig.class); + when(validationConfig.isEnabled()).thenReturn(true); + + final ThingValidationConfig thingValidationConfig = mock(ThingValidationConfig.class); + when(thingValidationConfig.isEnforceAttributes()).thenReturn(true); + when(thingValidationConfig.isForbidNonModeledAttributes()).thenReturn(true); + when(thingValidationConfig.isEnforceInboxMessagesInput()).thenReturn(true); + when(thingValidationConfig.isEnforceInboxMessagesOutput()).thenReturn(true); + when(thingValidationConfig.isEnforceOutboxMessages()).thenReturn(true); + when(thingValidationConfig.isForbidNonModeledInboxMessages()).thenReturn(true); + when(thingValidationConfig.isForbidNonModeledOutboxMessages()).thenReturn(true); + when(validationConfig.getThingValidationConfig()).thenReturn(thingValidationConfig); + + sut = WotThingModelValidation.of(validationConfig); + } + + @Test + public void validateThingAttributeSucceedsForBooleanAttribute() { + checkValidateThingAttribute(PROP_SOME_BOOL, JsonValue.of(true), false); + } + + @Test + public void validateThingAttributesSucceedsForBooleanAttribute() { + checkValidateThingAttributes(PROP_SOME_BOOL, JsonValue.of(true), false); + } + + @Test + public void validateThingAttributeFailsForBooleanAttribute() { + checkValidateThingAttribute(PROP_SOME_BOOL, JsonValue.of("something else"), true); + } + + @Test + public void validateThingAttributesFailsForBooleanAttribute() { + checkValidateThingAttributes(PROP_SOME_BOOL, JsonValue.of("something else"), true); + } + + @Test + public void validateThingAttributeSucceedsForIntegerAttribute() { + checkValidateThingAttribute(PROP_SOME_INT, JsonValue.of(42), false); + } + + @Test + public void validateThingAttributesSucceedsForIntegerAttribute() { + checkValidateThingAttributes(PROP_SOME_INT, JsonValue.of(42), false); + } + + @Test + public void validateThingAttributeFailsForIntegerAttribute() { + checkValidateThingAttribute(PROP_SOME_INT, JsonValue.of("something else"), true); + } + + @Test + public void validateThingAttributesFailsForIntegerAttribute() { + checkValidateThingAttributes(PROP_SOME_INT, JsonValue.of("something else"), true); + } + + @Test + public void validateThingAttributeSucceedsForNumberAttribute() { + checkValidateThingAttribute(PROP_SOME_NUMBER, JsonValue.of(42.23), false); + } + + @Test + public void validateThingAttributesSucceedsForNumberAttribute() { + checkValidateThingAttributes(PROP_SOME_NUMBER, JsonValue.of(42.23), false); + } + + @Test + public void validateThingAttributeFailsForNumberAttribute() { + checkValidateThingAttribute(PROP_SOME_NUMBER, JsonValue.of("something else"), true); + } + + @Test + public void validateThingAttributesFailsForNumberAttribute() { + checkValidateThingAttributes(PROP_SOME_NUMBER, JsonValue.of("something else"), true); + } + + @Test + public void validateThingAttributeSucceedsForStringAttribute() { + checkValidateThingAttribute(PROP_SOME_STRING, JsonValue.of("some"), false); + } + + @Test + public void validateThingAttributesSucceedsForStringAttribute() { + checkValidateThingAttributes(PROP_SOME_STRING, JsonValue.of("some"), false); + } + + @Test + public void validateThingAttributeFailsForStringAttribute() { + checkValidateThingAttribute(PROP_SOME_STRING, JsonValue.of(false), true); + } + + @Test + public void validateThingAttributesFailsForStringAttribute() { + checkValidateThingAttributes(PROP_SOME_STRING, JsonValue.of(false), true); + } + + @Test + public void validateThingAttributeSucceedsForArraysStringAttribute() { + checkValidateThingAttribute(PROP_SOME_ARRAY_STRINGS, JsonArray.of("some", "string", "arr"), false); + } + + @Test + public void validateThingAttributesSucceedsForArraysStringAttribute() { + checkValidateThingAttributes(PROP_SOME_ARRAY_STRINGS, JsonArray.of("some", "string", "arr"), false); + } + + @Test + public void validateThingAttributeFailsForArraysStringAttribute() { + checkValidateThingAttribute(PROP_SOME_ARRAY_STRINGS, JsonArray.of(false, true, false), true); + } + + @Test + public void validateThingAttributesFailsForArraysStringAttribute() { + checkValidateThingAttributes(PROP_SOME_ARRAY_STRINGS, JsonArray.of(false, true, false), true); + } + + @Test + public void validateThingAttributeSucceedsForObjectAttribute() { + checkValidateThingAttribute(PROP_SOME_OBJECT, PROP_KNOWN_SOME_OBJECT, false); + } + + @Test + public void validateThingAttributesSucceedsForObjectAttribute() { + checkValidateThingAttributes(PROP_SOME_OBJECT, PROP_KNOWN_SOME_OBJECT, false); + } + + @Test + public void validateThingAttributeFailsForObjectAttribute() { + checkValidateThingAttribute(PROP_SOME_OBJECT, JsonValue.of(false), true); + } + + @Test + public void validateThingAttributesFailsForObjectAttribute() { + checkValidateThingAttributes(PROP_SOME_OBJECT, JsonValue.of(false), true); + } + + @Test + public void validateThingAttributeFailsForObjectAttributeMissingRequiredField() { + checkValidateThingAttribute(PROP_SOME_ARRAY_STRINGS, + PROP_KNOWN_SOME_OBJECT.toBuilder().remove(PROP_SOME_STRING).build(), true); + } + + @Test + public void validateThingAttributesFailsForObjectAttributeMissingRequiredField() { + checkValidateThingAttributes(PROP_SOME_ARRAY_STRINGS, + PROP_KNOWN_SOME_OBJECT.toBuilder().remove(PROP_SOME_STRING).build(), true); + } + + @Test + public void validateThingAttributeFailsForObjectAttributeAdditionalNotSpecifiedField() { + checkValidateThingAttribute(PROP_SOME_ARRAY_STRINGS, + PROP_KNOWN_SOME_OBJECT.toBuilder().set("new", "foo").build(), true); + } + + @Test + public void validateThingAttributesFailsForObjectAttributeAdditionalNotSpecifiedField() { + checkValidateThingAttributes(PROP_SOME_ARRAY_STRINGS, + PROP_KNOWN_SOME_OBJECT.toBuilder().set("new", "foo").build(), true); + } + + @Test + public void validateThingAttributeFailsForNonModeledAttribute() { + checkValidateThingAttribute("newStuff", JsonValue.of(true), true); + } + + @Test + public void validateThingAttributesFailsForNonModeledAttribute() { + checkValidateThingAttributes("newStuff", JsonValue.of(true), true); + } + + @Test + public void validateThingAttributeDeletionFailsForRequiredBooleanAttribute() { + checkValidateThingAttributeDeletion(PROP_SOME_BOOL, true); + } + + @Test + public void validateThingAttributeDeletionSucceedsForOptionalArrayAttribute() { + checkValidateThingAttributeDeletion(PROP_SOME_ARRAY_STRINGS, false); + } + + @Test + public void validateThingActionBoolInputSucceeds() { + checkValidateThingActionInput(ACTION_PROCESS_BOOL, JsonValue.of(true), false); + } + + @Test + public void validateThingActionBoolInputFailsWrongDatatype() { + checkValidateThingActionInput(ACTION_PROCESS_BOOL, JsonValue.of("oh no"), true); + } + + @Test + public void validateThingActionObjectInputSucceeds() { + checkValidateThingActionInput(ACTION_PROCESS_OBJECT, JsonObject.newBuilder() + .set(PROP_SOME_BOOL, false) + .set(PROP_SOME_INT, 23) + .set(PROP_SOME_STRING, "yes!") + .build(), false); + } + + @Test + public void validateThingActionObjectInputFailsWrongDatatype() { + checkValidateThingActionInput(ACTION_PROCESS_OBJECT, JsonArray.empty(), true); + } + + @Test + public void validateThingActionObjectInputFailsMissingRequiredField() { + checkValidateThingActionInput(ACTION_PROCESS_OBJECT, JsonObject.newBuilder() + .set(PROP_SOME_STRING, "yes!") + .build(), true); + } + + @Test + public void validateNonModeledThingActionInputFails() { + checkValidateThingActionInput("someNewAction", JsonValue.of(true), true); + } + + @Test + public void validateThingActionBoolOutputSucceeds() { + checkValidateThingActionOutput(ACTION_PROCESS_BOOL, JsonValue.of(true), false); + } + + @Test + public void validateThingActionBoolOutputFailsWrongDatatype() { + checkValidateThingActionOutput(ACTION_PROCESS_BOOL, JsonValue.of("oh no"), true); + } + + @Test + public void validateThingActionObjectOutputSucceeds() { + checkValidateThingActionOutput(ACTION_PROCESS_OBJECT, JsonObject.newBuilder() + .set(PROP_SOME_BOOL, false) + .set(PROP_SOME_INT, 23) + .set(PROP_SOME_STRING, "yes!") + .build(), false); + } + + @Test + public void validateThingActionObjectOutputFailsWrongDatatype() { + checkValidateThingActionOutput(ACTION_PROCESS_OBJECT, JsonArray.empty(), true); + } + + @Test + public void validateThingActionObjectOutputFailsMissingRequiredField() { + checkValidateThingActionOutput(ACTION_PROCESS_OBJECT, JsonObject.newBuilder() + .set(PROP_SOME_STRING, "yes!") + .build(), true); + } + + @Test + public void validateThingEventIntDataSucceeds() { + checkValidateThingEventData(EVENT_EMIT_INT, JsonValue.of(33), false); + } + + @Test + public void validateThingEventBoolDataFailsWrongDatatype() { + checkValidateThingEventData(EVENT_EMIT_INT, JsonValue.of("oh no"), true); + } + + @Test + public void validateThingEventArrayDataSucceeds() { + checkValidateThingEventData(EVENT_EMIT_ARRAY, JsonArray.newBuilder() + .add(JsonObject.newBuilder() + .set(PROP_SOME_BOOL, false) + .set(PROP_SOME_INT, 23) + .set(PROP_SOME_STRING, "yes!") + .build() + ).build(), false); + } + + @Test + public void validateThingEventArrayDataFailsWrongDatatype() { + checkValidateThingEventData(EVENT_EMIT_ARRAY, JsonObject.empty(), true); + } + + @Test + public void validateThingEventArrayDataFailsMissingRequiredFieldInsideObject() { + checkValidateThingEventData(EVENT_EMIT_ARRAY, JsonArray.newBuilder() + .add(JsonObject.newBuilder() + .set(PROP_SOME_INT, 23) + .set(PROP_SOME_STRING, "yes!") + .build() + ).build(), true); + } + + @Test + public void validateNonModeledThingEventDataFails() { + checkValidateThingEventData("someNewEvent", JsonValue.of(true), true); + } + + private void checkValidateThingAttribute(final CharSequence attributePath, final JsonValue attributeValue, + final boolean mustFail + ) { + final JsonPointer attributePointer = JsonPointer.of(attributePath); + internalCheckFail(mustFail, sut.validateThingAttribute(KNOWN_THING_LEVEL_TM, + attributePointer, + attributeValue, + JsonPointer.of("attributes").append(attributePointer), + provideValidationContext() + )); + } + + private void checkValidateThingAttributes(final CharSequence attributePath, final JsonValue attributeValue, + final boolean mustFail + ) { + internalCheckFail(mustFail, sut.validateThingAttributes(KNOWN_THING_LEVEL_TM, + KNOWN_THING_ATTRIBUTES.toBuilder() + .set(attributePath, attributeValue) + .build(), + JsonPointer.of("attributes"), + provideValidationContext() + )); + } + + private void checkValidateThingAttributeDeletion(final CharSequence attributePath, final boolean mustFail) { + internalCheckFail(mustFail, sut.validateThingScopedDeletion(KNOWN_THING_LEVEL_TM, + Map.of(), + JsonPointer.of("attributes/" + attributePath), + provideValidationContext() + )); + } + + private void checkValidateThingActionInput(final String actionName, @Nullable final JsonValue inputData, + final boolean mustFail + ) { + internalCheckFail(mustFail, sut.validateThingActionInput(KNOWN_THING_LEVEL_TM, + actionName, + inputData, + JsonPointer.of("inbox/messages/" + actionName), + provideValidationContext() + )); + } + + private void checkValidateThingActionOutput(final String actionName, @Nullable final JsonValue outputData, + final boolean mustFail + ) { + internalCheckFail(mustFail, sut.validateThingActionOutput(KNOWN_THING_LEVEL_TM, + actionName, + outputData, + JsonPointer.of("inbox/messages/" + actionName), + provideValidationContext() + )); + } + + private void checkValidateThingEventData(final String eventName, @Nullable final JsonValue data, + final boolean mustFail + ) { + internalCheckFail(mustFail, sut.validateThingEventData(KNOWN_THING_LEVEL_TM, + eventName, + data, + JsonPointer.of("outbox/messages/" + eventName), + provideValidationContext() + )); + } + + private static ValidationContext provideValidationContext() { + return ValidationContext.buildValidationContext(DittoHeaders.empty(), null, null); + } + + private static void internalCheckFail(final boolean mustFail, final CompletionStage stage) { + if (mustFail) { + assertThat(stage) + .isCompletedExceptionally() + .failsWithin(50, TimeUnit.MILLISECONDS) + .withThrowableOfType(ExecutionException.class) + .withCauseInstanceOf(WotThingModelPayloadValidationException.class); + } else { + assertThat(stage).isNotCompletedExceptionally().isDone(); + } + } +} \ No newline at end of file