Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Add migrateDefinition IT #12

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ public enum HttpResource {

WHOAMI("/whoami"),

CHECK_PERMISSIONS("/checkPermissions");
CHECK_PERMISSIONS("/checkPermissions"),

MIGRATE_DEFINITION("/migrateDefinition");

private final String path;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1171,6 +1171,19 @@ protected static DeleteMatcher deleteDefinition(final int version, final CharSeq
return delete(dittoUrl(version, path)).withLogging(LOGGER, "FeatureDefinition");
}

/**
* Sends a POST request to the /migrateDefinition endpoint with the specified JSON payload.
*
* @param payload the JSON payload to post.
* @return the wrapped request.
*/
protected static PostMatcher postMigrateDefinition(final CharSequence thingId, @Nullable final String payload, final boolean dryRun) {
final String path = ResourcePathBuilder.forThing(thingId).migrateDefinition().toString();
return post(dittoUrl(TestConstants.API_V_2, path), payload)
.withParam("dry-run", String.valueOf(dryRun))
.withLogging(LOGGER, "MigrateDefinition");
}

/**
* Puts a Feature's Properties.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,11 @@ public Thing definition() {
return this;
}

public Thing migrateDefinition() {
stringBuilder.append(HttpResource.MIGRATE_DEFINITION);
return this;
}

}

public final class Policy extends AbstractPathBuilder {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@
import org.eclipse.ditto.things.model.signals.commands.modify.DeleteThing;
import org.eclipse.ditto.things.model.signals.commands.modify.MergeThing;
import org.eclipse.ditto.things.model.signals.commands.modify.MergeThingResponse;
import org.eclipse.ditto.things.model.signals.commands.modify.MigrateThingDefinition;
import org.eclipse.ditto.things.model.signals.commands.modify.MigrateThingDefinitionResponse;
import org.eclipse.ditto.things.model.signals.commands.modify.ModifyAttribute;
import org.eclipse.ditto.things.model.signals.commands.modify.ModifyAttributeResponse;
import org.eclipse.ditto.things.model.signals.commands.modify.ModifyFeature;
Expand All @@ -149,6 +151,7 @@
import org.eclipse.ditto.things.model.signals.events.ThingDeleted;
import org.eclipse.ditto.things.model.signals.events.ThingEvent;
import org.eclipse.ditto.things.model.signals.events.ThingMerged;
import org.eclipse.ditto.things.model.signals.events.ThingDefinitionMigrated;
import org.eclipse.ditto.things.model.signals.events.ThingModified;
import org.eclipse.ditto.thingsearch.model.signals.commands.subscription.CreateSubscription;
import org.eclipse.ditto.thingsearch.model.signals.commands.subscription.RequestFromSubscription;
Expand All @@ -175,6 +178,7 @@ public abstract class AbstractConnectivityITestCases<C, M> extends
private static final String EMPTY_MOD = "EMPTY_MOD";
private static final String FILTER_FOR_LIFECYCLE_EVENTS_BY_RQL = "FILTER_FOR_LIFECYCLE_EVENTS_BY_RQL";
private static final String FILTER_POLICY_ANNOUNCEMENTS_BY_NAMESPACE = "FILTER_BY_NAMESPACE";
private static final String THING_DEFINITION_URL = "https://eclipse-ditto.github.io/ditto-examples/wot/models/dimmable-colored-lamp-1.0.0.tm.jsonld";

static {
// adds filtering for twin events - only "created" and "deleted" ones
Expand Down Expand Up @@ -331,6 +335,64 @@ public void receiveSubjectDeletionNotification() {
assertThat(deleteAnnouncement.getSubjectIds()).containsExactly(subjectId);
}

@Test
@Category(RequireSource.class)
@Connections({CONNECTION1, CONNECTION2})
public void sendMigrateThingDefinitionAndEnsureEventsAreProduced() {
// Given
final String correlationId = createNewCorrelationId();
final ThingId thingId = generateThingId();
final Thing thing = Thing.newBuilder()
.setId(thingId)
.build();
final Policy policy = Policy.newBuilder()
.forLabel("DEFAULT")
.setSubject(testingContextWithRandomNs.getOAuthClient().getDefaultSubject())
.setSubject(connectionSubject(cf.connectionName1))
.setGrantedPermissions(PoliciesResourceType.thingResource("/"), READ, WRITE)
.setGrantedPermissions(PoliciesResourceType.policyResource("/"), READ, WRITE)
.setGrantedPermissions(PoliciesResourceType.messageResource("/"), READ, WRITE)
.forLabel("RESTRICTED")
.setSubject(connectionSubject(cf.connectionName2))
.setGrantedPermissions(PoliciesResourceType.thingResource("/"), READ)
.setGrantedPermissions(PoliciesResourceType.policyResource("/"), READ)
.setGrantedPermissions(PoliciesResourceType.messageResource("/"), READ)
.build();

final JsonObject migrationPayload = JsonObject.newBuilder()
.set(MigrateThingDefinition.JsonFields.JSON_THING_DEFINITION_URL, THING_DEFINITION_URL)
.build();
final MigrateThingDefinition migrateThingDefinition = MigrateThingDefinition.of(
thingId, THING_DEFINITION_URL, migrationPayload, Collections.emptyMap(), true,createDittoHeaders(correlationId));

// When
final C eventConsumer = initTargetsConsumer(cf.connectionName2);
putThingWithPolicy(2, thing, policy, JsonSchemaVersion.V_2)
.withCorrelationId(correlationId)
.withJWT(testingContextWithRandomNs.getOAuthClient().getAccessToken())
.expectingHttpStatus(HttpStatus.CREATED)
.fire();

final CommandResponse<?> commandResponse =
sendCommandAndEnsureResponseIsSentBack(cf.connectionName1, migrateThingDefinition);
LOGGER.info("Received response: {}", commandResponse);
assertThat(commandResponse).isInstanceOf(MigrateThingDefinitionResponse.class);
assertThat(commandResponse.getHttpStatus()).isEqualTo(HttpStatus.OK);

// Then wait for all events generated (order independent)
consumeAndAssertEvents(cf.connectionName2, eventConsumer, Arrays.asList(
e -> {
LOGGER.info("Received event: {}", e);
final ThingCreated tc = thingEventForJson(e, ThingCreated.class, correlationId, thingId);
assertThat(tc.getRevision()).isEqualTo(1L);
},
e -> {
LOGGER.info("Received event: {}", e);
final ThingDefinitionMigrated tm = thingEventForJson(e, ThingDefinitionMigrated.class, correlationId, thingId);
assertThat(tm.getRevision()).isEqualTo(2L);
}), "ThingCreated", "ThingModified");
}

@Test
@UseConnection(category = ConnectionCategory.CONNECTION1, mod = FILTER_POLICY_ANNOUNCEMENTS_BY_NAMESPACE)
public void receiveSubjectDeletionNotificationForNamespace() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/*
* Copyright (c) 2025 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.testing.system.things.rest;

import org.eclipse.ditto.base.model.common.HttpStatus;
import org.eclipse.ditto.base.model.json.JsonSchemaVersion;
import org.eclipse.ditto.json.JsonFactory;
import org.eclipse.ditto.json.JsonObject;
import org.eclipse.ditto.json.JsonPointer;
import org.eclipse.ditto.json.JsonValue;
import org.eclipse.ditto.policies.api.Permission;
import org.eclipse.ditto.policies.model.PoliciesResourceType;
import org.eclipse.ditto.policies.model.Policy;
import org.eclipse.ditto.policies.model.Subject;
import org.eclipse.ditto.policies.model.SubjectIssuer;
import org.eclipse.ditto.testing.common.IntegrationTest;
import org.eclipse.ditto.testing.common.TestConstants;
import org.eclipse.ditto.testing.common.client.BasicAuth;
import org.eclipse.ditto.things.model.Thing;
import org.eclipse.ditto.things.model.ThingId;
import org.eclipse.ditto.things.model.ThingsModelFactory;
import org.eclipse.ditto.things.model.signals.commands.ThingCommand;
import org.eclipse.ditto.things.model.signals.commands.modify.MigrateThingDefinition;
import org.eclipse.ditto.things.model.signals.commands.modify.MigrateThingDefinitionResponse;
import org.junit.BeforeClass;
import org.junit.Test;
import org.eclipse.ditto.policies.model.Subjects;
import org.junit.FixMethodOrder;
import org.junit.runners.MethodSorters;

@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public final class MigrateThingDefinitionIT extends IntegrationTest {

private static ThingId testThingId;
private static Policy testPolicy;
private static final String THING_DEFINITION_URL = "https://eclipse-ditto.github.io/ditto-examples/wot/models/dimmable-colored-lamp-1.0.0.tm.jsonld";

@BeforeClass
public static void setup() {
testThingId = ThingId.of(idGenerator().withRandomName());
testPolicy = createPolicyForMigration();

putThingWithPolicy(TestConstants.API_V_2, newThing(testThingId), testPolicy, JsonSchemaVersion.V_2)
.expectingHttpStatus(HttpStatus.CREATED)
.fire();
}

@Test
public void test1_MigrateDefinitionDryRun() {
final JsonObject migrationPayload = buildMigrationPayload();

postMigrateDefinition(testThingId, migrationPayload.toString(), true)
.expectingHttpStatus(HttpStatus.ACCEPTED)
.expectingBody(contains(buildExpectedResponse(testThingId, true)))
.fire();
}

@Test
public void test2_MigrateDefinitionSuccess() {
final JsonObject migrationPayload = buildMigrationPayload();

postMigrateDefinition(testThingId, migrationPayload.toString(), false)
.expectingHttpStatus(HttpStatus.OK)
.expectingBody(contains(buildExpectedResponse(testThingId, false)))
.fire();
}

@Test
public void test4_MigrateDefinitionWithWotValidationError() {
final JsonObject migrationPayload = JsonObject.newBuilder()
.set(MigrateThingDefinition.JsonFields.JSON_THING_DEFINITION_URL, THING_DEFINITION_URL)
.set(MigrateThingDefinition.JsonFields.JSON_MIGRATION_PAYLOAD, JsonFactory.newObjectBuilder()
.set("attributes", JsonFactory.newObjectBuilder()
.set("dimmer-level", JsonFactory.nullLiteral())
.build())
.build())
.build();

postMigrateDefinition(testThingId, migrationPayload.toString(), false)
.expectingHttpStatus(HttpStatus.BAD_REQUEST)
.expectingBody(contains(JsonObject.newBuilder().set("error", "wot:payload.validation.error").build()))
.fire();
}

@Test
public void test5_MigrateDefinitionWithWotValidationError() {
final JsonObject migrationPayload = JsonObject.newBuilder()
.set(MigrateThingDefinition.JsonFields.JSON_THING_DEFINITION_URL, THING_DEFINITION_URL)
.set(MigrateThingDefinition.JsonFields.JSON_MIGRATION_PAYLOAD, JsonFactory.newObjectBuilder()
.set("attributes", JsonFactory.newObjectBuilder()
.set("dimmer-level", 0.5)
.build())
.build())
.set(MigrateThingDefinition.JsonFields.JSON_PATCH_CONDITIONS, JsonObject.newBuilder()
.set("thing:/features/thermostat", "eq(attributes/dimmer-level,1.0)")
.build())
.build();

postMigrateDefinition(testThingId, migrationPayload.toString(), false)
.expectingHttpStatus(HttpStatus.OK)
.expectingBody(contains(JsonObject.newBuilder()
.set(ThingCommand.JsonFields.JSON_THING_ID, testThingId.toString())
.set(MigrateThingDefinitionResponse.JsonFields.JSON_PATCH, JsonFactory.newObjectBuilder()
.set("definition", THING_DEFINITION_URL)
.set("attributes", JsonFactory.newObjectBuilder()
.set("dimmer-level", 0.5)
.build())
.build())
.set(MigrateThingDefinitionResponse.JsonFields.JSON_MERGE_STATUS, "APPLIED")
.build()))
.fire();
}

@Test
public void test6_MigrateDefinitionWithInvalidThingId() {
final JsonObject migrationPayload = buildMigrationPayload();

postMigrateDefinition(serviceEnv.getDefaultNamespaceName() + ":unknownThingId", migrationPayload.toString(), false)
.expectingHttpStatus(HttpStatus.NOT_FOUND)
.fire();
}

@Test
public void test7_MigrateDefinitionWithInvalidPayload() {
final JsonObject invalidPayload = JsonFactory.newObjectBuilder()
.set("invalid_field", "some_value")
.build();

postMigrateDefinition(testThingId, invalidPayload.toString(), false)
.expectingHttpStatus(HttpStatus.BAD_REQUEST)
.fire();
}

private static Thing newThing(final ThingId thingId) {
return ThingsModelFactory.newThingBuilder()
.setId(thingId)
.setAttribute(JsonPointer.of("manufacturer"), JsonValue.of("Old Corp"))
.build();
}

private static JsonObject buildMigrationPayload() {
return JsonObject.newBuilder()
.set(MigrateThingDefinition.JsonFields.JSON_THING_DEFINITION_URL, THING_DEFINITION_URL)
.set(MigrateThingDefinition.JsonFields.JSON_MIGRATION_PAYLOAD, JsonFactory.newObjectBuilder()
.set("attributes", JsonFactory.newObjectBuilder()
.set("manufacturer", JsonFactory.nullLiteral())
.set("color", JsonFactory.newObjectBuilder()
.set("g", 5)
.build())
.set("dimmer-level", 1.0)
.build())
.build())
.set(MigrateThingDefinition.JsonFields.JSON_INITIALIZE_MISSING_PROPERTIES_FROM_DEFAULTS, true)
.build();
}

private static JsonObject buildExpectedResponse(ThingId thingId, boolean dryRun) {
return JsonObject.newBuilder()
.set(ThingCommand.JsonFields.JSON_THING_ID, thingId.toString())
.set(MigrateThingDefinitionResponse.JsonFields.JSON_PATCH, JsonFactory.newObjectBuilder()
.set("definition", THING_DEFINITION_URL)
.set("attributes", JsonFactory.newObjectBuilder()
.set("manufacturer", JsonFactory.nullLiteral())
.set("on", false)
.set("color", JsonFactory.newObjectBuilder()
.set("r", 0)
.set("g", 5)
.set("b", 0)
.build())
.set("dimmer-level", 1.0)
.build())
.build())
.set(MigrateThingDefinitionResponse.JsonFields.JSON_MERGE_STATUS, dryRun ? "DRY_RUN" : "APPLIED")
.build();
}

private static Policy createPolicyForMigration() {
final BasicAuth basicAuth = serviceEnv.getDefaultTestingContext().getBasicAuth();
final Subjects subjects;
if (basicAuth.isEnabled()) {
subjects = Subjects.newInstance(Subject.newInstance(
SubjectIssuer.newInstance("nginx"), basicAuth.getUsername()));
} else {
subjects = Subjects.newInstance(
serviceEnv.getDefaultTestingContext().getOAuthClient().getSubject(),
serviceEnv.getTestingContext2().getOAuthClient().getSubject());
}
return Policy.newBuilder()
.forLabel("DEFAULT")
.setSubjects(subjects)
.setGrantedPermissions(PoliciesResourceType.thingResource("/"), Permission.WRITE, Permission.READ)
.setGrantedPermissions(PoliciesResourceType.policyResource("/"), Permission.WRITE, Permission.READ)
.build();
}
}
Loading