25 changed files with 1249 additions and 560 deletions
File diff suppressed because one or more lines are too long
@ -1,94 +0,0 @@ |
|||
{ |
|||
"widgetsBundle": { |
|||
"alias": "indoor_environment", |
|||
"title": "Indoor Environment", |
|||
"image": "data:image/png;base64,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", |
|||
"description": "Contains widgets displaying indoor environment telemetry.", |
|||
"order": 10000, |
|||
"externalId": null, |
|||
"name": "Indoor Environment" |
|||
}, |
|||
"widgetTypeFqns": [ |
|||
"indoor_temperature_card", |
|||
"indoor_temperature_card_with_background", |
|||
"indoor_horizontal_temperature_card", |
|||
"indoor_horizontal_temperature_card_with_background", |
|||
"indoor_temperature_chart_card", |
|||
"indoor_temperature_chart_card_with_background", |
|||
"indoor_simple_temperature_chart_card", |
|||
"indoor_simple_temperature_chart_card_with_background", |
|||
"indoor_temperature_progress_bar", |
|||
"indoor_temperature_progress_bar_with_background", |
|||
"indoor_temperature_range_chart", |
|||
"indoor_temperature_range_chart_with_background", |
|||
"indoor_temperature_gauge", |
|||
"indoor_humidity_card", |
|||
"indoor_humidity_card_with_background", |
|||
"indoor_horizontal_humidity_card", |
|||
"indoor_horizontal_humidity_card_with_background", |
|||
"indoor_humidity_chart_card", |
|||
"indoor_humidity_chart_card_with_background", |
|||
"indoor_simple_humidity_chart_card", |
|||
"indoor_simple_humidity_chart_card_with_background", |
|||
"indoor_humidity_progress_bar", |
|||
"indoor_humidity_progress_bar_with_background", |
|||
"indoor_co2_card", |
|||
"indoor_co2_card_with_background", |
|||
"indoor_horizontal_co2_card", |
|||
"indoor_horizontal_co2_card_with_background", |
|||
"indoor_co2_chart_card", |
|||
"indoor_co2_chart_card_with_background", |
|||
"indoor_simple_co2_chart_card", |
|||
"indoor_simple_co2_chart_card_with_background", |
|||
"indoor_illuminance_card", |
|||
"indoor_illuminance_card_with_background", |
|||
"indoor_horizontal_illuminance_card", |
|||
"indoor_horizontal_illuminance_card_with_background", |
|||
"indoor_illuminance_chart_card", |
|||
"indoor_illuminance_chart_card_with_background", |
|||
"indoor_simple_illuminance_chart_card", |
|||
"indoor_simple_illuminance_chart_card_with_background", |
|||
"indoor_illuminance_progress_bar", |
|||
"indoor_illuminance_progress_bar_with_background", |
|||
"noise_level_card", |
|||
"noise_level_card_with_background", |
|||
"horizontal_noise_level_card", |
|||
"horizontal_noise_level_card_with_background", |
|||
"noise_level_chart_card", |
|||
"noise_level_chart_card_with_background", |
|||
"simple_noise_level_chart_card", |
|||
"simple_noise_level_chart_card_with_background", |
|||
"indoor_pm2_5_card", |
|||
"indoor_pm2_5_card_with_background", |
|||
"indoor_horizontal_pm2_5_card", |
|||
"indoor_horizontal_pm2_5_card_with_background", |
|||
"indoor_pm2_5_chart_card", |
|||
"indoor_pm2_5_chart_card_with_background", |
|||
"indoor_simple_pm2_5_chart_card", |
|||
"indoor_simple_pm2_5_chart_card_with_background", |
|||
"indoor_pm10_card", |
|||
"indoor_pm10_card_with_background", |
|||
"indoor_horizontal_pm10_card", |
|||
"indoor_horizontal_pm10_card_with_background", |
|||
"indoor_pm10_chart_card", |
|||
"indoor_pm10_chart_card_with_background", |
|||
"indoor_simple_pm10_chart_card", |
|||
"indoor_simple_pm10_chart_card_with_background", |
|||
"radon_level_card", |
|||
"radon_level_card_with_background", |
|||
"horizontal_radon_level_card", |
|||
"horizontal_radon_level_card_with_background", |
|||
"radon_level_chart_card", |
|||
"radon_level_chart_card_with_background", |
|||
"simple_radon_level_chart_card", |
|||
"simple_radon_level_chart_card_with_background", |
|||
"volatile_organic_compounds_card", |
|||
"volatile_organic_compounds_card_with_background", |
|||
"horizontal_volatile_organic_compounds_card", |
|||
"horizontal_volatile_organic_compounds_card_with_background", |
|||
"volatile_organic_compounds_chart_card", |
|||
"volatile_organic_compounds_chart_card_with_background", |
|||
"simple_volatile_organic_compounds_chart_card", |
|||
"simple_volatile_organic_compounds_chart_card_with_background" |
|||
] |
|||
} |
|||
File diff suppressed because one or more lines are too long
@ -1,190 +0,0 @@ |
|||
{ |
|||
"widgetsBundle": { |
|||
"alias": "outdoor_environment", |
|||
"title": "Outdoor Environment", |
|||
"image": "data:image/png;base64,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", |
|||
"description": "Contains widgets displaying outdoor environment telemetry.", |
|||
"order": 11000, |
|||
"externalId": null, |
|||
"name": "Outdoor Environment" |
|||
}, |
|||
"widgetTypeFqns": [ |
|||
"temperature_card", |
|||
"temperature_card_with_background", |
|||
"horizontal_temperature_card", |
|||
"horizontal_temperature_card_with_background", |
|||
"temperature_chart_card", |
|||
"temperature_chart_card_with_background", |
|||
"simple_temperature_chart_card", |
|||
"simple_temperature_chart_card_with_background", |
|||
"temperature_range_chart", |
|||
"temperature_range_chart_with_background", |
|||
"temperature_gauge", |
|||
"humidity_card", |
|||
"humidity_card_with_background", |
|||
"horizontal_humidity_card", |
|||
"horizontal_humidity_card_with_background", |
|||
"humidity_chart_card", |
|||
"humidity_chart_card_with_background", |
|||
"simple_humidity_chart_card", |
|||
"simple_humidity_chart_card_with_background", |
|||
"humidity_progress_bar", |
|||
"humidity_progress_bar_with_background", |
|||
"pressure_card", |
|||
"pressure_card_with_background", |
|||
"horizontal_pressure_card", |
|||
"horizontal_pressure_card_with_background", |
|||
"pressure_chart_card", |
|||
"pressure_chart_card_with_background", |
|||
"simple_pressure_chart_card", |
|||
"simple_pressure_chart_card_with_background", |
|||
"pressure_progress_bar", |
|||
"pressure_progress_bar_with_background", |
|||
"wind_speed_card", |
|||
"wind_speed_card_with_background", |
|||
"horizontal_wind_speed_card", |
|||
"horizontal_wind_speed_card_with_background", |
|||
"wind_speed_chart_card", |
|||
"wind_speed_chart_card_with_background", |
|||
"simple_wind_speed_chart_card", |
|||
"simple_wind_speed_chart_card_with_background", |
|||
"wind_speed_and_direction", |
|||
"wind_speed_and_direction_with_background", |
|||
"rainfall_card", |
|||
"rainfall_card_with_background", |
|||
"horizontal_rainfall_card", |
|||
"horizontal_rainfall_card_with_background", |
|||
"rainfall_chart_card", |
|||
"rainfall_chart_card_with_background", |
|||
"simple_rainfall_chart_card", |
|||
"simple_rainfall_chart_card_with_background", |
|||
"solar_radiation_card", |
|||
"solar_radiation_card_with_background", |
|||
"horizontal_solar_radiation_card", |
|||
"horizontal_solar_radiation_card_with_background", |
|||
"solar_radiation_chart_card", |
|||
"solar_radiation_chart_card_with_background", |
|||
"simple_solar_radiation_chart_card", |
|||
"simple_solar_radiation_chart_card_with_background", |
|||
"uv_index_card", |
|||
"uv_index_card_with_background", |
|||
"horizontal_uv_index_card", |
|||
"horizontal_uv_index_card_with_background", |
|||
"uv_index_chart_card", |
|||
"uv_index_chart_card_with_background", |
|||
"simple_uv_index_chart_card", |
|||
"simple_uv_index_chart_card_with_background", |
|||
"air_quality_card", |
|||
"air_quality_card_with_background", |
|||
"horizontal_air_quality_card", |
|||
"horizontal_air_quality_card_with_background", |
|||
"air_quality_chart_card", |
|||
"air_quality_chart_card_with_background", |
|||
"simple_air_quality_chart_card", |
|||
"simple_air_quality_chart_card_with_background", |
|||
"visibility_card", |
|||
"visibility_card_with_background", |
|||
"horizontal_visibility_card", |
|||
"horizontal_visibility_card_with_background", |
|||
"visibility_chart_card", |
|||
"visibility_chart_card_with_background", |
|||
"simple_visibility_chart_card", |
|||
"simple_visibility_chart_card_with_background", |
|||
"ground_temperature_card", |
|||
"ground_temperature_card_with_background", |
|||
"horizontal_ground_temperature_card", |
|||
"horizontal_ground_temperature_card_with_background", |
|||
"ground_temperature_chart_card", |
|||
"ground_temperature_chart_card_with_background", |
|||
"simple_ground_temperature_chart_card", |
|||
"simple_ground_temperature_chart_card_with_background", |
|||
"soil_moisture_card", |
|||
"soil_moisture_card_with_background", |
|||
"horizontal_soil_moisture_card", |
|||
"horizontal_soil_moisture_card_with_background", |
|||
"soil_moisture_chart_card", |
|||
"soil_moisture_chart_card_with_background", |
|||
"simple_soil_moisture_chart_card", |
|||
"simple_soil_moisture_chart_card_with_background", |
|||
"soil_moisture_progress_bar", |
|||
"soil_moisture_progress_bar_with_background", |
|||
"noise_level_card", |
|||
"noise_level_card_with_background", |
|||
"horizontal_noise_level_card", |
|||
"horizontal_noise_level_card_with_background", |
|||
"noise_level_chart_card", |
|||
"noise_level_chart_card_with_background", |
|||
"simple_noise_level_chart_card", |
|||
"simple_noise_level_chart_card_with_background", |
|||
"vibration_card", |
|||
"vibration_card_with_background", |
|||
"horizontal_vibration_card", |
|||
"horizontal_vibration_card_with_background", |
|||
"vibration_chart_card", |
|||
"vibration_chart_card_with_background", |
|||
"simple_vibration_chart_card", |
|||
"simple_vibration_chart_card_with_background", |
|||
"leaf_wetness_card", |
|||
"leaf_wetness_card_with_background", |
|||
"horizontal_leaf_wetness_card", |
|||
"horizontal_leaf_wetness_card_with_background", |
|||
"leaf_wetness_chart_card", |
|||
"leaf_wetness_chart_card_with_background", |
|||
"simple_leaf_wetness_chart_card", |
|||
"simple_leaf_wetness_chart_card_with_background", |
|||
"leaf_wetness_progress_bar", |
|||
"leaf_wetness_progress_bar_with_background", |
|||
"snow_depth_card", |
|||
"snow_depth_card_with_background", |
|||
"horizontal_snow_depth_card", |
|||
"horizontal_snow_depth_card_with_background", |
|||
"snow_depth_chart_card", |
|||
"snow_depth_chart_card_with_background", |
|||
"simple_snow_depth_chart_card", |
|||
"simple_snow_depth_chart_card_with_background", |
|||
"co2_card", |
|||
"co2_card_with_background", |
|||
"horizontal_co2_card", |
|||
"horizontal_co2_card_with_background", |
|||
"co2_chart_card", |
|||
"co2_chart_card_with_background", |
|||
"simple_co2_chart_card", |
|||
"simple_co2_chart_card_with_background", |
|||
"flooding_level_card", |
|||
"flooding_level_card_with_background", |
|||
"horizontal_flooding_level_card", |
|||
"horizontal_flooding_level_card_with_background", |
|||
"flooding_level_chart_card", |
|||
"flooding_level_chart_card_with_background", |
|||
"simple_flooding_level_chart_card", |
|||
"simple_flooding_level_chart_card_with_background", |
|||
"flooding_level_progress_bar", |
|||
"flooding_level_progress_bar_with_background", |
|||
"illuminance_card", |
|||
"illuminance_card_with_background", |
|||
"horizontal_illuminance_card", |
|||
"horizontal_illuminance_card_with_background", |
|||
"illuminance_chart_card", |
|||
"illuminance_chart_card_with_background", |
|||
"simple_illuminance_chart_card", |
|||
"simple_illuminance_chart_card_with_background", |
|||
"illuminance_progress_bar", |
|||
"illuminance_progress_bar_with_background", |
|||
"pm2_5_card", |
|||
"pm2_5_card_with_background", |
|||
"horizontal_pm2_5_card", |
|||
"horizontal_pm2_5_card_with_background", |
|||
"pm2_5_chart_card", |
|||
"pm2_5_chart_card_with_background", |
|||
"simple_pm2_5_chart_card", |
|||
"simple_pm2_5_chart_card_with_background", |
|||
"pm10_card", |
|||
"pm10_card_with_background", |
|||
"horizontal_pm10_card", |
|||
"horizontal_pm10_card_with_background", |
|||
"pm10_chart_card", |
|||
"pm10_chart_card_with_background", |
|||
"simple_pm10_chart_card", |
|||
"simple_pm10_chart_card_with_background" |
|||
] |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
/** |
|||
* Copyright © 2016-2026 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.iot_hub; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
import java.util.List; |
|||
|
|||
@Data |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class InstallPlan { |
|||
|
|||
private String rootVersionId; |
|||
private List<InstallPlanEntry> entries; |
|||
|
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
/** |
|||
* Copyright © 2016-2026 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.iot_hub; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
@Data |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class InstallPlanEntry { |
|||
|
|||
public enum Status { |
|||
WILL_INSTALL, |
|||
ALREADY_INSTALLED, |
|||
MISSING |
|||
} |
|||
|
|||
private String itemId; |
|||
private String versionId; |
|||
private String name; |
|||
private String type; |
|||
private String version; |
|||
private Status status; |
|||
private boolean root; |
|||
private String errorMessage; |
|||
|
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
/** |
|||
* Copyright © 2016-2026 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.iot_hub; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItemDescriptor; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
@Data |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class InstallPlanResult { |
|||
|
|||
private boolean success; |
|||
private boolean rolledBack; |
|||
private String errorMessage; |
|||
private IotHubInstalledItemDescriptor rootDescriptor; |
|||
private List<InstallPlanEntry> entries = new ArrayList<>(); |
|||
private List<String> missingItemIds = new ArrayList<>(); |
|||
|
|||
} |
|||
@ -0,0 +1,368 @@ |
|||
/** |
|||
* Copyright © 2016-2026 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.iot_hub; |
|||
|
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import com.fasterxml.jackson.databind.node.ObjectNode; |
|||
import jakarta.servlet.http.HttpServletRequest; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.api.extension.ExtendWith; |
|||
import org.mockito.InOrder; |
|||
import org.mockito.InjectMocks; |
|||
import org.mockito.Mock; |
|||
import org.mockito.Spy; |
|||
import org.mockito.junit.jupiter.MockitoExtension; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.server.common.data.id.IotHubInstalledItemId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.iot_hub.DashboardInstalledItemDescriptor; |
|||
import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem; |
|||
import org.thingsboard.server.dao.iot_hub.IotHubInstalledItemService; |
|||
import org.thingsboard.server.service.security.model.SecurityUser; |
|||
|
|||
import java.util.List; |
|||
import java.util.UUID; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
|||
import static org.mockito.ArgumentMatchers.any; |
|||
import static org.mockito.ArgumentMatchers.anyString; |
|||
import static org.mockito.ArgumentMatchers.eq; |
|||
import static org.mockito.ArgumentMatchers.isNull; |
|||
import static org.mockito.Mockito.doNothing; |
|||
import static org.mockito.Mockito.doReturn; |
|||
import static org.mockito.Mockito.doThrow; |
|||
import static org.mockito.Mockito.inOrder; |
|||
import static org.mockito.Mockito.mock; |
|||
import static org.mockito.Mockito.never; |
|||
import static org.mockito.Mockito.verify; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
@ExtendWith(MockitoExtension.class) |
|||
class DefaultIotHubServiceTest { |
|||
|
|||
private final TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); |
|||
private final SecurityUser user = mock(SecurityUser.class); |
|||
|
|||
@Mock |
|||
private IotHubRestClient iotHubRestClient; |
|||
@Mock |
|||
private IotHubInstalledItemService iotHubInstalledItemService; |
|||
|
|||
@Spy |
|||
@InjectMocks |
|||
private DefaultIotHubService service; |
|||
|
|||
private void mockTenant() { |
|||
when(user.getTenantId()).thenReturn(tenantId); |
|||
} |
|||
|
|||
private IotHubInstalledItem installedItem(UUID id) { |
|||
IotHubInstalledItem item = new IotHubInstalledItem(); |
|||
item.setId(new IotHubInstalledItemId(id)); |
|||
item.setTenantId(tenantId); |
|||
return item; |
|||
} |
|||
|
|||
private InstallPlanEntry willInstall(String versionId, boolean root) { |
|||
InstallPlanEntry entry = new InstallPlanEntry(); |
|||
entry.setItemId(UUID.randomUUID().toString()); |
|||
entry.setVersionId(versionId); |
|||
entry.setName(root ? "Root" : "Dep"); |
|||
entry.setStatus(InstallPlanEntry.Status.WILL_INSTALL); |
|||
entry.setRoot(root); |
|||
return entry; |
|||
} |
|||
|
|||
private ObjectNode version(String id, String itemId, String type, String name, String ver) { |
|||
ObjectNode node = JacksonUtil.newObjectNode(); |
|||
node.put("id", id); |
|||
node.put("itemId", itemId); |
|||
node.put("type", type); |
|||
node.put("name", name); |
|||
node.put("version", ver); |
|||
return node; |
|||
} |
|||
|
|||
@Test |
|||
void resolveInstallPlan_rootOnly_noRelated_marksWillInstall() { |
|||
mockTenant(); |
|||
String versionId = UUID.randomUUID().toString(); |
|||
String itemId = UUID.randomUUID().toString(); |
|||
when(iotHubRestClient.getVersionInfo(versionId)).thenReturn(version(versionId, itemId, "DASHBOARD", "Root", "1.0")); |
|||
when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); |
|||
|
|||
InstallPlan plan = service.resolveInstallPlan(user, versionId); |
|||
|
|||
assertThat(plan.getEntries()).hasSize(1); |
|||
InstallPlanEntry root = plan.getEntries().get(0); |
|||
assertThat(root.isRoot()).isTrue(); |
|||
assertThat(root.getStatus()).isEqualTo(InstallPlanEntry.Status.WILL_INSTALL); |
|||
assertThat(root.getItemId()).isEqualTo(itemId); |
|||
} |
|||
|
|||
@Test |
|||
void resolveInstallPlan_ordersDependenciesBeforeRoot() { |
|||
mockTenant(); |
|||
String versionId = UUID.randomUUID().toString(); |
|||
String rootItemId = UUID.randomUUID().toString(); |
|||
String relatedItemId = UUID.randomUUID().toString(); |
|||
ObjectNode root = version(versionId, rootItemId, "DASHBOARD", "Root", "1.0"); |
|||
root.putArray("relatedItems").add(relatedItemId); |
|||
when(iotHubRestClient.getVersionInfo(versionId)).thenReturn(root); |
|||
when(iotHubRestClient.getPublishedVersionByItemId(relatedItemId)) |
|||
.thenReturn(version(UUID.randomUUID().toString(), relatedItemId, "WIDGET", "Dep", "2.0")); |
|||
when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); |
|||
|
|||
InstallPlan plan = service.resolveInstallPlan(user, versionId); |
|||
|
|||
assertThat(plan.getEntries()).hasSize(2); |
|||
assertThat(plan.getEntries().get(0).getItemId()).isEqualTo(relatedItemId); |
|||
assertThat(plan.getEntries().get(0).isRoot()).isFalse(); |
|||
assertThat(plan.getEntries().get(1).getItemId()).isEqualTo(rootItemId); |
|||
assertThat(plan.getEntries().get(1).isRoot()).isTrue(); |
|||
} |
|||
|
|||
@Test |
|||
void resolveInstallPlan_missingRelated_recordedAsMissing() { |
|||
mockTenant(); |
|||
String versionId = UUID.randomUUID().toString(); |
|||
String rootItemId = UUID.randomUUID().toString(); |
|||
String relatedItemId = UUID.randomUUID().toString(); |
|||
ObjectNode root = version(versionId, rootItemId, "DASHBOARD", "Root", "1.0"); |
|||
root.putArray("relatedItems").add(relatedItemId); |
|||
when(iotHubRestClient.getVersionInfo(versionId)).thenReturn(root); |
|||
when(iotHubRestClient.getPublishedVersionByItemId(relatedItemId)).thenReturn(null); |
|||
when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); |
|||
|
|||
InstallPlan plan = service.resolveInstallPlan(user, versionId); |
|||
|
|||
InstallPlanEntry missing = plan.getEntries().get(0); |
|||
assertThat(missing.getStatus()).isEqualTo(InstallPlanEntry.Status.MISSING); |
|||
assertThat(missing.getItemId()).isEqualTo(relatedItemId); |
|||
assertThat(missing.getErrorMessage()).isNotBlank(); |
|||
} |
|||
|
|||
@Test |
|||
void resolveInstallPlan_relatedFetchThrows_recordedAsMissing() { |
|||
mockTenant(); |
|||
String versionId = UUID.randomUUID().toString(); |
|||
String rootItemId = UUID.randomUUID().toString(); |
|||
String relatedItemId = UUID.randomUUID().toString(); |
|||
ObjectNode root = version(versionId, rootItemId, "DASHBOARD", "Root", "1.0"); |
|||
root.putArray("relatedItems").add(relatedItemId); |
|||
when(iotHubRestClient.getVersionInfo(versionId)).thenReturn(root); |
|||
when(iotHubRestClient.getPublishedVersionByItemId(relatedItemId)).thenThrow(new RuntimeException("boom")); |
|||
when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); |
|||
|
|||
InstallPlan plan = service.resolveInstallPlan(user, versionId); |
|||
|
|||
assertThat(plan.getEntries().get(0).getStatus()).isEqualTo(InstallPlanEntry.Status.MISSING); |
|||
assertThat(plan.getEntries().get(0).getErrorMessage()).contains("boom"); |
|||
} |
|||
|
|||
@Test |
|||
void resolveInstallPlan_alreadyInstalledRoot_marksAlreadyInstalled() { |
|||
mockTenant(); |
|||
String versionId = UUID.randomUUID().toString(); |
|||
UUID rootItemId = UUID.randomUUID(); |
|||
when(iotHubRestClient.getVersionInfo(versionId)) |
|||
.thenReturn(version(versionId, rootItemId.toString(), "DASHBOARD", "Root", "1.0")); |
|||
when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of(rootItemId)); |
|||
|
|||
InstallPlan plan = service.resolveInstallPlan(user, versionId); |
|||
|
|||
assertThat(plan.getEntries().get(0).getStatus()).isEqualTo(InstallPlanEntry.Status.ALREADY_INSTALLED); |
|||
} |
|||
|
|||
@Test |
|||
void resolveInstallPlan_nullVersion_throws() { |
|||
mockTenant(); |
|||
String versionId = UUID.randomUUID().toString(); |
|||
when(iotHubRestClient.getVersionInfo(versionId)).thenReturn(null); |
|||
|
|||
assertThatThrownBy(() -> service.resolveInstallPlan(user, versionId)) |
|||
.isInstanceOf(IllegalArgumentException.class); |
|||
} |
|||
|
|||
@Test |
|||
void installPlan_emptyPlan_returnsFailureWithoutInstalling() { |
|||
InstallPlanResult result = service.installPlan(user, new InstallPlan(null, List.of()), null, null); |
|||
|
|||
assertThat(result.isSuccess()).isFalse(); |
|||
assertThat(result.getErrorMessage()).contains("empty"); |
|||
verify(iotHubInstalledItemService, never()).findInstalledItemIdsByTenantIdAndItemIdIn(any(), any()); |
|||
} |
|||
|
|||
@Test |
|||
void installPlan_missingEntry_collectedAndNotInstalled() { |
|||
mockTenant(); |
|||
InstallPlanEntry missing = new InstallPlanEntry(); |
|||
missing.setItemId(UUID.randomUUID().toString()); |
|||
missing.setStatus(InstallPlanEntry.Status.MISSING); |
|||
|
|||
InstallPlanResult result = service.installPlan(user, new InstallPlan(null, List.of(missing)), null, null); |
|||
|
|||
assertThat(result.isSuccess()).isTrue(); |
|||
assertThat(result.getMissingItemIds()).containsExactly(missing.getItemId()); |
|||
assertThat(result.getRootDescriptor()).isNull(); |
|||
verify(iotHubRestClient, never()).getVersionInfo(anyString()); |
|||
// A plan with nothing to install never hits the DB to check what is already installed.
|
|||
verify(iotHubInstalledItemService, never()).findInstalledItemIdsByTenantIdAndItemIdIn(any(), any()); |
|||
} |
|||
|
|||
@Test |
|||
void installPlan_stalePlan_skipsItemInstalledSinceResolution() { |
|||
mockTenant(); |
|||
UUID itemId = UUID.randomUUID(); |
|||
InstallPlanEntry entry = new InstallPlanEntry(); |
|||
entry.setItemId(itemId.toString()); |
|||
entry.setVersionId(UUID.randomUUID().toString()); |
|||
entry.setStatus(InstallPlanEntry.Status.WILL_INSTALL); |
|||
entry.setRoot(true); |
|||
// The item is reported as already installed at install time, even though the plan says WILL_INSTALL.
|
|||
when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of(itemId)); |
|||
|
|||
InstallPlanResult result = service.installPlan(user, new InstallPlan(null, List.of(entry)), null, null); |
|||
|
|||
assertThat(result.isSuccess()).isTrue(); |
|||
assertThat(result.getEntries().get(0).getStatus()).isEqualTo(InstallPlanEntry.Status.ALREADY_INSTALLED); |
|||
// No marketplace fetch happened — the duplicate install was avoided.
|
|||
verify(iotHubRestClient, never()).getVersionInfo(anyString()); |
|||
} |
|||
|
|||
@Test |
|||
void installPlan_duplicateItemId_installsOnce() throws Exception { |
|||
mockTenant(); |
|||
HttpServletRequest request = mock(HttpServletRequest.class); |
|||
UUID itemId = UUID.randomUUID(); |
|||
String firstVersionId = UUID.randomUUID().toString(); |
|||
String secondVersionId = UUID.randomUUID().toString(); |
|||
// Two entries point at the same item — a client-supplied plan is not de-duplicated, so the
|
|||
// cascade itself must not install the same item twice.
|
|||
InstallPlanEntry first = new InstallPlanEntry(); |
|||
first.setItemId(itemId.toString()); |
|||
first.setVersionId(firstVersionId); |
|||
first.setStatus(InstallPlanEntry.Status.WILL_INSTALL); |
|||
first.setRoot(true); |
|||
InstallPlanEntry duplicate = new InstallPlanEntry(); |
|||
duplicate.setItemId(itemId.toString()); |
|||
duplicate.setVersionId(secondVersionId); |
|||
duplicate.setStatus(InstallPlanEntry.Status.WILL_INSTALL); |
|||
duplicate.setRoot(false); |
|||
|
|||
// Nothing is installed yet when the plan is submitted.
|
|||
when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); |
|||
doReturn(installedItem(UUID.randomUUID())).when(service).doInstallVersion(eq(user), eq(firstVersionId), any(), any()); |
|||
|
|||
InstallPlanResult result = service.installPlan(user, new InstallPlan(firstVersionId, List.of(first, duplicate)), null, request); |
|||
|
|||
assertThat(result.isSuccess()).isTrue(); |
|||
assertThat(result.getEntries()).hasSize(2); |
|||
assertThat(result.getEntries().get(0).getStatus()).isEqualTo(InstallPlanEntry.Status.WILL_INSTALL); |
|||
// The duplicate is recognised as freshly installed and skipped.
|
|||
assertThat(result.getEntries().get(1).getStatus()).isEqualTo(InstallPlanEntry.Status.ALREADY_INSTALLED); |
|||
verify(service).doInstallVersion(eq(user), eq(firstVersionId), any(), any()); |
|||
verify(service, never()).doInstallVersion(eq(user), eq(secondVersionId), any(), any()); |
|||
} |
|||
|
|||
@Test |
|||
void installPlan_cascade_installsDependencyBeforeRoot_andRoutesDataToRootOnly() throws Exception { |
|||
mockTenant(); |
|||
HttpServletRequest request = mock(HttpServletRequest.class); |
|||
JsonNode data = JacksonUtil.newObjectNode().put("entityId", UUID.randomUUID().toString()); |
|||
String depVersionId = UUID.randomUUID().toString(); |
|||
String rootVersionId = UUID.randomUUID().toString(); |
|||
InstallPlanEntry dep = willInstall(depVersionId, false); |
|||
InstallPlanEntry root = willInstall(rootVersionId, true); |
|||
|
|||
when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); |
|||
IotHubInstalledItem depItem = installedItem(UUID.randomUUID()); |
|||
IotHubInstalledItem rootItem = installedItem(UUID.randomUUID()); |
|||
DashboardInstalledItemDescriptor rootDescriptor = new DashboardInstalledItemDescriptor(); |
|||
rootItem.setDescriptor(rootDescriptor); |
|||
doReturn(depItem).when(service).doInstallVersion(eq(user), eq(depVersionId), any(), any()); |
|||
doReturn(rootItem).when(service).doInstallVersion(eq(user), eq(rootVersionId), any(), any()); |
|||
|
|||
InstallPlanResult result = service.installPlan(user, new InstallPlan(rootVersionId, List.of(dep, root)), data, request); |
|||
|
|||
assertThat(result.isSuccess()).isTrue(); |
|||
assertThat(result.isRolledBack()).isFalse(); |
|||
assertThat(result.getRootDescriptor()).isSameAs(rootDescriptor); |
|||
assertThat(result.getMissingItemIds()).isEmpty(); |
|||
assertThat(result.getEntries()).hasSize(2); |
|||
|
|||
// The dependency installs before the root, and only the root receives the user's install data.
|
|||
InOrder inOrder = inOrder(service); |
|||
inOrder.verify(service).doInstallVersion(eq(user), eq(depVersionId), isNull(), eq(request)); |
|||
inOrder.verify(service).doInstallVersion(eq(user), eq(rootVersionId), eq(data), eq(request)); |
|||
verify(service, never()).deleteInstalledItem(any(), any()); |
|||
} |
|||
|
|||
@Test |
|||
void installPlan_dependencyFails_rollsBackInstalledItemsInReverseOrder() throws Exception { |
|||
mockTenant(); |
|||
HttpServletRequest request = mock(HttpServletRequest.class); |
|||
String dep1VersionId = UUID.randomUUID().toString(); |
|||
String dep2VersionId = UUID.randomUUID().toString(); |
|||
String rootVersionId = UUID.randomUUID().toString(); |
|||
InstallPlanEntry dep1 = willInstall(dep1VersionId, false); |
|||
InstallPlanEntry dep2 = willInstall(dep2VersionId, false); |
|||
InstallPlanEntry root = willInstall(rootVersionId, true); |
|||
|
|||
when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); |
|||
IotHubInstalledItem dep1Item = installedItem(UUID.randomUUID()); |
|||
IotHubInstalledItem dep2Item = installedItem(UUID.randomUUID()); |
|||
doReturn(dep1Item).when(service).doInstallVersion(eq(user), eq(dep1VersionId), any(), any()); |
|||
doReturn(dep2Item).when(service).doInstallVersion(eq(user), eq(dep2VersionId), any(), any()); |
|||
doThrow(new IllegalStateException("install failed")).when(service).doInstallVersion(eq(user), eq(rootVersionId), any(), any()); |
|||
doNothing().when(service).deleteInstalledItem(eq(user), any()); |
|||
|
|||
InstallPlanResult result = service.installPlan(user, new InstallPlan(rootVersionId, List.of(dep1, dep2, root)), null, request); |
|||
|
|||
assertThat(result.isSuccess()).isFalse(); |
|||
assertThat(result.isRolledBack()).isTrue(); |
|||
assertThat(result.getErrorMessage()).contains("install failed"); |
|||
|
|||
// Successfully installed dependencies are rolled back in reverse install order (dep2 before dep1).
|
|||
InOrder inOrder = inOrder(service); |
|||
inOrder.verify(service).deleteInstalledItem(user, dep2Item.getId()); |
|||
inOrder.verify(service).deleteInstalledItem(user, dep1Item.getId()); |
|||
} |
|||
|
|||
@Test |
|||
void installPlan_rollbackFailure_reportsNotFullyRolledBack() throws Exception { |
|||
mockTenant(); |
|||
HttpServletRequest request = mock(HttpServletRequest.class); |
|||
String depVersionId = UUID.randomUUID().toString(); |
|||
String rootVersionId = UUID.randomUUID().toString(); |
|||
InstallPlanEntry dep = willInstall(depVersionId, false); |
|||
InstallPlanEntry root = willInstall(rootVersionId, true); |
|||
|
|||
when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); |
|||
IotHubInstalledItem depItem = installedItem(UUID.randomUUID()); |
|||
doReturn(depItem).when(service).doInstallVersion(eq(user), eq(depVersionId), any(), any()); |
|||
doThrow(new IllegalStateException("install failed")).when(service).doInstallVersion(eq(user), eq(rootVersionId), any(), any()); |
|||
// Rolling back the one installed dependency itself fails — the result must report a partial rollback.
|
|||
doThrow(new RuntimeException("delete failed")).when(service).deleteInstalledItem(user, depItem.getId()); |
|||
|
|||
InstallPlanResult result = service.installPlan(user, new InstallPlan(rootVersionId, List.of(dep, root)), null, request); |
|||
|
|||
assertThat(result.isSuccess()).isFalse(); |
|||
assertThat(result.isRolledBack()).isFalse(); |
|||
assertThat(result.getErrorMessage()).contains("install failed"); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue