diff --git a/src/integrationtests/java/com/aws/greengrass/integrationtests/RateLimiterTest.java b/src/integrationtests/java/com/aws/greengrass/integrationtests/RateLimiterTest.java index 2d0eacb4..2e43e178 100644 --- a/src/integrationtests/java/com/aws/greengrass/integrationtests/RateLimiterTest.java +++ b/src/integrationtests/java/com/aws/greengrass/integrationtests/RateLimiterTest.java @@ -98,9 +98,10 @@ void GIVEN_throttled_cloud_update_requests_WHEN_cloud_updates_THEN_cloud_updates when(mockUpdateThingShadowResponse.payload()).thenReturn(SdkBytes.fromString("{\"version\": 1}", UTF_8)); when(iotDataPlaneClientFactory.getIotDataPlaneClient().updateThingShadow(any(software.amazon.awssdk.services.iotdataplane.model.UpdateThingShadowRequest.class))) .thenReturn(mockUpdateThingShadowResponse); + ShadowDocument shadowDocument = new ShadowDocument(localShadowContentV1.getBytes()); // mock dao calls in cloud update - when(dao.getShadowThing(anyString(), anyString())).thenReturn(Optional.of(new ShadowDocument(localShadowContentV1.getBytes()))); + when(dao.getShadowThing(anyString(), anyString())).thenReturn(Optional.of(shadowDocument)); when(dao.getShadowSyncInformation(anyString(), anyString())).thenReturn( Optional.of(SyncInformation.builder() .lastSyncedDocument(lastSyncedDocument.getBytes()) @@ -118,7 +119,7 @@ void GIVEN_throttled_cloud_update_requests_WHEN_cloud_updates_THEN_cloud_updates // thingName has to be unique to prevent requests from being merged final int totalRequestCalls = 10; for (int i = 0; i < totalRequestCalls; i++) { - syncHandler.pushCloudUpdateSyncRequest(String.valueOf(i), CLASSIC_SHADOW_IDENTIFIER, updateDocument); + syncHandler.pushCloudUpdateSyncRequest(String.valueOf(i), CLASSIC_SHADOW_IDENTIFIER, updateDocument, shadowDocument); } // verify that some requests have been throttled diff --git a/src/integrationtests/java/com/aws/greengrass/integrationtests/SyncTest.java b/src/integrationtests/java/com/aws/greengrass/integrationtests/SyncTest.java index 8c3c589c..9e92b3ab 100644 --- a/src/integrationtests/java/com/aws/greengrass/integrationtests/SyncTest.java +++ b/src/integrationtests/java/com/aws/greengrass/integrationtests/SyncTest.java @@ -532,7 +532,7 @@ void GIVEN_synced_shadow_WHEN_multiple_local_updates_THEN_cloud_updates(Class syncInfo.get().isPresent(), eventuallyEval(is(true))); assertThat("local shadow exists", localShadow.get().isPresent(), is(true)); ShadowDocument shadowDocument = localShadow.get().get(); @@ -607,7 +607,7 @@ void GIVEN_synced_shadow_WHEN_multiple_cloud_and_local_received_THEN_cloud_updat assertEmptySyncQueue(clazz); verify(syncHandler, after(10000).times(4)) - .pushCloudUpdateSyncRequest(anyString(), anyString(), any(JsonNode.class)); + .pushCloudUpdateSyncRequest(anyString(), anyString(), any(JsonNode.class), any(ShadowDocument.class)); assertThat("sync info exists", () -> syncInfo.get().isPresent(), eventuallyEval(is(true))); assertThat("local shadow exists", localShadow.get().isPresent(), is(true)); ShadowDocument shadowDocument = localShadow.get().get(); @@ -890,7 +890,7 @@ void GIVEN_unsynced_shadow_WHEN_local_updates_THEN_no_cloud_update(Class shadowDocument = context.getDao().getShadowThing(getThingName(), getShadowName()); - //TODO: store this information in a return object to avoid unnecessary calls to DAO. SyncInformation currentSyncInformation = context.getDao() .getShadowSyncInformation(getThingName(), getShadowName()) .orElseThrow(() -> new UnknownShadowException("Shadow not found in sync table")); - if (!isUpdateNecessary(shadowDocument, currentSyncInformation, context)) { + if (!isUpdateNecessary(currentSyncInformation, context)) { return; } @@ -150,13 +154,13 @@ public void execute(SyncContext context) throws RetryableException, SkipSyncRequ try { context.getDao().updateSyncInformation(SyncInformation.builder() - .lastSyncedDocument(JsonUtil.getPayloadBytes(shadowDocument.get().toJson(false))) + .lastSyncedDocument(JsonUtil.getPayloadBytes(localShadowDocument.toJson(false))) .cloudVersion(cloudUpdatedVersion) .cloudDeleted(false) .shadowName(getShadowName()) .thingName(getThingName()) - .cloudUpdateTime(shadowDocument.get().getMetadata().getLatestUpdatedTimestamp()) - .localVersion(shadowDocument.get().getVersion()) + .cloudUpdateTime(localShadowDocument.getMetadata().getLatestUpdatedTimestamp()) + .localVersion(localShadowDocument.getVersion()) .build()); } catch (JsonProcessingException | ShadowManagerDataException e) { logger.atError() @@ -178,20 +182,18 @@ public void execute(SyncContext context) throws RetryableException, SkipSyncRequ */ @Override boolean isUpdateNecessary(SyncContext context) throws SkipSyncRequestException, UnknownShadowException { - Optional shadowDocument = context.getDao().getShadowThing(getThingName(), getShadowName()); - //TODO: store this information in a return object to avoid unnecessary calls to DAO. SyncInformation currentSyncInformation = context.getDao() .getShadowSyncInformation(getThingName(), getShadowName()) .orElseThrow(() -> new UnknownShadowException("Shadow not found in sync table")); - return isUpdateNecessary(shadowDocument, currentSyncInformation, context); + return isUpdateNecessary(currentSyncInformation, context); } - private boolean isUpdateNecessary(Optional shadowDocument, SyncInformation currentSyncInformation, + private boolean isUpdateNecessary(SyncInformation currentSyncInformation, SyncContext context) throws SkipSyncRequestException { - if (!shadowDocument.isPresent()) { + if (Objects.isNull(localShadowDocument)) { logger.atDebug() .kv(LOG_THING_NAME_KEY, getThingName()) .kv(LOG_SHADOW_NAME_KEY, getShadowName()) @@ -208,7 +210,7 @@ private boolean isUpdateNecessary(Optional shadowDocument, SyncI .kv(LOG_LOCAL_VERSION_KEY, currentSyncInformation.getLocalVersion()) .kv(LOG_CLOUD_VERSION_KEY, currentSyncInformation.getCloudVersion()) .log("Cloud shadow already contains update payload. No sync is necessary"); - updateSyncInformationVersion(shadowDocument, currentSyncInformation, context); + updateSyncInformationVersion(Optional.of(localShadowDocument), currentSyncInformation, context); return false; } diff --git a/src/test/java/com/aws/greengrass/shadowmanager/ipc/UpdateThingShadowRequestHandlerTest.java b/src/test/java/com/aws/greengrass/shadowmanager/ipc/UpdateThingShadowRequestHandlerTest.java index db57f49f..bbacaad3 100644 --- a/src/test/java/com/aws/greengrass/shadowmanager/ipc/UpdateThingShadowRequestHandlerTest.java +++ b/src/test/java/com/aws/greengrass/shadowmanager/ipc/UpdateThingShadowRequestHandlerTest.java @@ -745,7 +745,7 @@ void GIVEN_update_document_with_delta_WHEN_handle_request_THEN_delta_is_dropped_ getJsonFromResource(RESOURCE_DIRECTORY_NAME + BAD_UPDATE_DOCUMENT_WITH_DELTA_FILE_NAME); UpdateThingShadowRequestHandler updateThingShadowIPCHandler = new UpdateThingShadowRequestHandler(mockDao, mockAuthorizationHandlerWrapper, mockPubSubClientWrapper, mockSynchronizeHelper, mockSyncHandler); ArgumentCaptor documentCaptor = ArgumentCaptor.forClass(JsonNode.class); - doNothing().when(mockSyncHandler).pushCloudUpdateSyncRequest(any(), any(), documentCaptor.capture()); + doNothing().when(mockSyncHandler).pushCloudUpdateSyncRequest(any(), any(), documentCaptor.capture(), any()); when(mockDao.updateShadowThing(any(), any(), any(), anyLong())) .thenReturn(Optional.of(new byte[]{})); diff --git a/src/test/java/com/aws/greengrass/shadowmanager/sync/SyncHandlerTest.java b/src/test/java/com/aws/greengrass/shadowmanager/sync/SyncHandlerTest.java index dce3145a..dcad1069 100644 --- a/src/test/java/com/aws/greengrass/shadowmanager/sync/SyncHandlerTest.java +++ b/src/test/java/com/aws/greengrass/shadowmanager/sync/SyncHandlerTest.java @@ -6,6 +6,7 @@ package com.aws.greengrass.shadowmanager.sync; +import com.aws.greengrass.shadowmanager.model.ShadowDocument; import com.aws.greengrass.shadowmanager.model.configuration.ThingShadowSyncConfiguration; import com.aws.greengrass.shadowmanager.sync.model.BaseSyncRequest; import com.aws.greengrass.shadowmanager.sync.model.CloudDeleteSyncRequest; @@ -137,7 +138,7 @@ void GIVEN_synced_shadows_WHEN_pushCloudUpdateSyncRequest_THEN_calls_overall_syn syncHandler.setSyncConfigurations(syncConfigurations); // WHEN - syncHandler.pushCloudUpdateSyncRequest("a", "1", mock(JsonNode.class)); + syncHandler.pushCloudUpdateSyncRequest("a", "1", mock(JsonNode.class), mock(ShadowDocument.class)); // THEN verify(mockSyncStrategy, times(1)).putSyncRequest(any()); diff --git a/src/test/java/com/aws/greengrass/shadowmanager/sync/model/CloudUpdateSyncRequestTest.java b/src/test/java/com/aws/greengrass/shadowmanager/sync/model/CloudUpdateSyncRequestTest.java index 49c6e713..3a3a140d 100644 --- a/src/test/java/com/aws/greengrass/shadowmanager/sync/model/CloudUpdateSyncRequestTest.java +++ b/src/test/java/com/aws/greengrass/shadowmanager/sync/model/CloudUpdateSyncRequestTest.java @@ -103,7 +103,6 @@ void GIVEN_good_cloud_update_request_WHEN_execute_THEN_successfully_updates_clou long epochSeconds = Instant.now().getEpochSecond(); long epochSecondsMinus60 = Instant.now().minusSeconds(60).getEpochSecond(); ShadowDocument shadowDocument = new ShadowDocument(BASE_DOCUMENT); - when(mockDao.getShadowThing(anyString(), anyString())).thenReturn(Optional.of(shadowDocument)); when(mockDao.getShadowSyncInformation(anyString(), anyString())).thenReturn(Optional.of(SyncInformation.builder() .cloudUpdateTime(epochSecondsMinus60) .thingName(THING_NAME) @@ -116,11 +115,10 @@ void GIVEN_good_cloud_update_request_WHEN_execute_THEN_successfully_updates_clou when(mockIotDataPlaneClientWrapper.updateThingShadow(anyString(), anyString(), any(byte[].class))) .thenReturn(UpdateThingShadowResponse.builder().payload(SdkBytes.fromString("{\"version\": 6}", UTF_8)).build()); - CloudUpdateSyncRequest request = new CloudUpdateSyncRequest(THING_NAME, SHADOW_NAME, baseDocumentJson); - + CloudUpdateSyncRequest request = new CloudUpdateSyncRequest(THING_NAME, SHADOW_NAME, baseDocumentJson, + shadowDocument); request.execute(mockContext); - verify(mockDao, times(1)).getShadowThing(anyString(), anyString()); verify(mockDao, times(1)).updateSyncInformation(any()); verify(mockIotDataPlaneClientWrapper, times(1)).updateThingShadow(anyString(), anyString(), any(byte[].class)); @@ -134,31 +132,17 @@ void GIVEN_good_cloud_update_request_WHEN_execute_THEN_successfully_updates_clou assertThat(syncInformationCaptor.getValue().isCloudDeleted(), is(false)); } - @Test - void GIVEN_cloud_update_request_for_non_existent_shadow_WHEN_execute_THEN_does_not_update_cloud_shadow_and_sync_information() throws Exception { - when(mockDao.getShadowThing(anyString(), anyString())).thenReturn(Optional.empty()); - CloudUpdateSyncRequest request = new CloudUpdateSyncRequest(THING_NAME, SHADOW_NAME, baseDocumentJson); - - request.execute(mockContext); - - verify(mockDao, times(1)).getShadowThing(anyString(), anyString()); - verify(mockDao, times(0)).updateSyncInformation(any()); - verify(mockIotDataPlaneClientWrapper, times(0)).updateThingShadow(anyString(), anyString(), any(byte[].class)); - } - @ParameterizedTest @ValueSource(classes = {ThrottlingException.class, ServiceUnavailableException.class, InternalFailureException.class}) void GIVEN_bad_cloud_update_request_WHEN_execute_and_updateShadow_throws_retryable_error_THEN_does_not_update_cloud_shadow_and_sync_information(Class clazz, ExtensionContext context) throws IOException, IoTDataPlaneClientCreationException { ignoreExceptionOfType(context, clazz); ShadowDocument shadowDocument = new ShadowDocument(BASE_DOCUMENT); - when(mockDao.getShadowThing(anyString(), anyString())).thenReturn(Optional.of(shadowDocument)); doThrow(clazz).when(mockIotDataPlaneClientWrapper).updateThingShadow(anyString(), anyString(), any(byte[].class)); - CloudUpdateSyncRequest request = new CloudUpdateSyncRequest(THING_NAME, SHADOW_NAME, baseDocumentJson); + CloudUpdateSyncRequest request = new CloudUpdateSyncRequest(THING_NAME, SHADOW_NAME, baseDocumentJson, shadowDocument); RetryableException thrown = assertThrows(RetryableException.class, () -> request.execute(mockContext)); assertThat(thrown.getCause(), is(instanceOf(clazz))); - verify(mockDao, times(1)).getShadowThing(anyString(), anyString()); verify(mockDao, times(0)).updateSyncInformation(any()); verify(mockIotDataPlaneClientWrapper, times(1)).updateThingShadow(anyString(), anyString(), any(byte[].class)); } @@ -167,16 +151,14 @@ void GIVEN_bad_cloud_update_request_WHEN_execute_and_updateShadow_throws_retryab void GIVEN_bad_cloud_update_request_WHEN_execute_and_updateShadow_throws_conflict_exception_THEN_does_not_update_cloud_shadow_and_sync_information(ExtensionContext context) throws IOException, IoTDataPlaneClientCreationException { ignoreExceptionOfType(context, ConflictException.class); ShadowDocument shadowDocument = new ShadowDocument(BASE_DOCUMENT); - when(mockDao.getShadowThing(anyString(), anyString())).thenReturn(Optional.of(shadowDocument)); doThrow(ConflictException.builder().message(SAMPLE_EXCEPTION_MESSAGE).build()) .when(mockIotDataPlaneClientWrapper).updateThingShadow(anyString(), anyString(), any(byte[].class)); - CloudUpdateSyncRequest request = new CloudUpdateSyncRequest(THING_NAME, SHADOW_NAME, baseDocumentJson); + CloudUpdateSyncRequest request = new CloudUpdateSyncRequest(THING_NAME, SHADOW_NAME, baseDocumentJson, shadowDocument); ConflictException thrown = assertThrows(ConflictException.class, () -> request.execute(mockContext)); assertThat(thrown.getMessage(), is(equalTo(SAMPLE_EXCEPTION_MESSAGE))); - verify(mockDao, times(1)).getShadowThing(anyString(), anyString()); verify(mockDao, times(0)).updateSyncInformation(any()); verify(mockIotDataPlaneClientWrapper, times(1)).updateThingShadow(anyString(), anyString(), any(byte[].class)); } @@ -187,15 +169,13 @@ void GIVEN_bad_cloud_update_request_WHEN_execute_and_updateShadow_throws_conflic void GIVEN_bad_cloud_update_request_WHEN_execute_and_updateShadow_throws_skipable_error_THEN_does_not_update_cloud_shadow_and_sync_information(Class clazz, ExtensionContext context) throws IOException, IoTDataPlaneClientCreationException { ignoreExceptionOfType(context, clazz); ShadowDocument shadowDocument = new ShadowDocument(BASE_DOCUMENT); - when(mockDao.getShadowThing(anyString(), anyString())).thenReturn(Optional.of(shadowDocument)); doThrow(clazz).when(mockIotDataPlaneClientWrapper).updateThingShadow(anyString(), anyString(), any(byte[].class)); - CloudUpdateSyncRequest request = new CloudUpdateSyncRequest(THING_NAME, SHADOW_NAME, baseDocumentJson); + CloudUpdateSyncRequest request = new CloudUpdateSyncRequest(THING_NAME, SHADOW_NAME, baseDocumentJson, shadowDocument); SkipSyncRequestException thrown = assertThrows(SkipSyncRequestException.class, () -> request.execute(mockContext)); assertThat(thrown.getCause(), is(instanceOf(clazz))); - verify(mockDao, times(1)).getShadowThing(anyString(), anyString()); verify(mockDao, times(0)).updateSyncInformation(any()); verify(mockIotDataPlaneClientWrapper, times(1)).updateThingShadow(anyString(), anyString(), any(byte[].class)); } @@ -203,9 +183,9 @@ void GIVEN_bad_cloud_update_request_WHEN_execute_and_updateShadow_throws_skipabl @Test void GIVEN_new_values_WHEN_merge_THEN_document_merged() throws IOException { - CloudUpdateSyncRequest request = new CloudUpdateSyncRequest(THING_NAME, SHADOW_NAME, baseDocumentJson); + CloudUpdateSyncRequest request = new CloudUpdateSyncRequest(THING_NAME, SHADOW_NAME, baseDocumentJson, null); JsonNode updateDocument = JsonUtil.getPayloadJson(UPDATE_DOCUMENT).get(); - CloudUpdateSyncRequest other = new CloudUpdateSyncRequest(THING_NAME, SHADOW_NAME, updateDocument); + CloudUpdateSyncRequest other = new CloudUpdateSyncRequest(THING_NAME, SHADOW_NAME, updateDocument, null); request.merge(other); assertThat(request.updateDocument, is(updateDocument)); @@ -215,7 +195,6 @@ void GIVEN_new_values_WHEN_merge_THEN_document_merged() throws IOException { void GIVEN_no_change_to_shadow_content_but_version_change_WHEN_isUpdateNecessary_THEN_returns_false_and_updates_sync_info() throws IOException, SkipSyncRequestException, UnknownShadowException { when(mockDao.updateSyncInformation(syncInformationCaptor.capture())).thenReturn(true); ShadowDocument shadowDocument = new ShadowDocument(BASE_DOCUMENT); - when(mockDao.getShadowThing(anyString(), anyString())).thenReturn(Optional.of(shadowDocument)); long epochSeconds = Instant.now().getEpochSecond(); when(mockDao.getShadowSyncInformation(anyString(), anyString())).thenReturn(Optional.of(SyncInformation.builder() @@ -228,7 +207,8 @@ void GIVEN_no_change_to_shadow_content_but_version_change_WHEN_isUpdateNecessary .localVersion(0L) .lastSyncTime(epochSeconds) .build())); - CloudUpdateSyncRequest request = new CloudUpdateSyncRequest(THING_NAME, SHADOW_NAME, baseDocumentJson); + CloudUpdateSyncRequest request = new CloudUpdateSyncRequest(THING_NAME, SHADOW_NAME, baseDocumentJson, + shadowDocument); assertFalse(request.isUpdateNecessary(mockContext)); verify(mockDao, atMostOnce()).updateSyncInformation(any()); @@ -247,7 +227,6 @@ void GIVEN_no_change_to_shadow_content_but_version_change_WHEN_isUpdateNecessary @Test void GIVEN_no_change_to_shadow_content_and_no_version_change_WHEN_isUpdateNecessary_THEN_returns_false_and_does_not_update_sync_info() throws IOException, SkipSyncRequestException, UnknownShadowException { ShadowDocument shadowDocument = new ShadowDocument(BASE_DOCUMENT); - when(mockDao.getShadowThing(anyString(), anyString())).thenReturn(Optional.of(shadowDocument)); long epochSeconds = Instant.now().getEpochSecond(); when(mockDao.getShadowSyncInformation(anyString(), anyString())).thenReturn(Optional.of(SyncInformation.builder() @@ -260,7 +239,7 @@ void GIVEN_no_change_to_shadow_content_and_no_version_change_WHEN_isUpdateNecess .localVersion(1L) .lastSyncTime(epochSeconds) .build())); - CloudUpdateSyncRequest request = new CloudUpdateSyncRequest(THING_NAME, SHADOW_NAME, baseDocumentJson); + CloudUpdateSyncRequest request = new CloudUpdateSyncRequest(THING_NAME, SHADOW_NAME, baseDocumentJson, shadowDocument); assertFalse(request.isUpdateNecessary(mockContext)); verify(mockDao, never()).updateSyncInformation(any()); @@ -269,13 +248,12 @@ void GIVEN_no_change_to_shadow_content_and_no_version_change_WHEN_isUpdateNecess @Test void GIVEN_new_shadow_WHEN_isUpdateNecessary_THEN_returns_true() throws IOException, UnknownShadowException, SkipSyncRequestException { ShadowDocument shadowDocument = new ShadowDocument(BASE_DOCUMENT); - when(mockDao.getShadowThing(anyString(), anyString())).thenReturn(Optional.of(shadowDocument)); when(mockDao.getShadowSyncInformation(anyString(), anyString())).thenReturn(Optional.of( SyncInformation.builder() .lastSyncedDocument(null) .build())); - CloudUpdateSyncRequest request = new CloudUpdateSyncRequest(THING_NAME, SHADOW_NAME, baseDocumentJson); + CloudUpdateSyncRequest request = new CloudUpdateSyncRequest(THING_NAME, SHADOW_NAME, baseDocumentJson, shadowDocument); assertTrue(request.isUpdateNecessary(mockContext)); } @@ -284,7 +262,8 @@ void GIVEN_new_shadow_WHEN_isUpdateNecessary_THEN_returns_true() throws IOExcept void GIVEN_different_cloud_update_WHEN_isUpdateNecessary_THEN_returns_true() throws IOException { JsonNode j1 = JsonUtil.getPayloadJson("{\"state\":{\"reported\":{\"color\":{\"r\":255,\"g\":0,\"b\":0},\"SomeKey\":\"SomeValue\"}},\"metadata\":{\"reported\":{\"color\":{\"r\":{\"timestamp\":1619722006},\"g\":{\"timestamp\":1619722006},\"b\":{\"timestamp\":1619722006}},\"SomeKey\":{\"timestamp\":1619722006}}},\"version\":1,\"timestamp\":1619722006}".getBytes()).get(); JsonNode j2 = JsonUtil.getPayloadJson("{\"state\":{\"reported\":{\"color\":{\"r\":255,\"g\":255,\"b\":255},\"SomeKey\":\"SomeValue\"}},\"metadata\":{\"reported\":{\"color\":{\"r\":{\"timestamp\":1619722006},\"g\":{\"timestamp\":1619722006},\"b\":{\"timestamp\":1619722006}},\"SomeKey\":{\"timestamp\":1619722006}}},\"version\":1,\"timestamp\":1619722006}".getBytes()).get(); - CloudUpdateSyncRequest request = new CloudUpdateSyncRequest(THING_NAME, SHADOW_NAME, baseDocumentJson); + CloudUpdateSyncRequest request = new CloudUpdateSyncRequest(THING_NAME, SHADOW_NAME, baseDocumentJson, + new ShadowDocument(BASE_DOCUMENT)); assertTrue(request.isUpdateNecessary(j1, j2)); } @@ -292,7 +271,7 @@ void GIVEN_different_cloud_update_WHEN_isUpdateNecessary_THEN_returns_true() thr void GIVEN_same_cloud_update_WHEN_isUpdateNecessary_THEN_returns_false() throws IOException { JsonNode j1 = JsonUtil.getPayloadJson("{\"state\":{\"reported\":{\"color\":{\"r\":255,\"g\":255,\"b\":255},\"SomeKey\":\"SomeValue\"}},\"metadata\":{\"reported\":{\"color\":{\"r\":{\"timestamp\":1619722006},\"g\":{\"timestamp\":1619722006},\"b\":{\"timestamp\":1619722006}},\"SomeKey\":{\"timestamp\":1619722006}}},\"version\":1,\"timestamp\":1619722006}".getBytes()).get(); JsonNode j2 = JsonUtil.getPayloadJson("{\"state\":{\"reported\":{\"color\":{\"r\":255,\"g\":255,\"b\":255},\"SomeKey\":\"SomeValue\"}},\"metadata\":{\"reported\":{\"color\":{\"r\":{\"timestamp\":1619722006},\"g\":{\"timestamp\":1619722006},\"b\":{\"timestamp\":1619722006}},\"SomeKey\":{\"timestamp\":1619722006}}},\"version\":1,\"timestamp\":1619722006}".getBytes()).get(); - CloudUpdateSyncRequest request = new CloudUpdateSyncRequest(THING_NAME, SHADOW_NAME, baseDocumentJson); + CloudUpdateSyncRequest request = new CloudUpdateSyncRequest(THING_NAME, SHADOW_NAME, baseDocumentJson, new ShadowDocument(BASE_DOCUMENT)); assertFalse(request.isUpdateNecessary(j1, j2)); } }