]> git.basschouten.com Git - openhab-addons.git/commitdiff
[boschshc] Boost unit test coverage (#16500)
authorDavid Pace <dev@davidpace.de>
Sun, 31 Mar 2024 19:19:25 +0000 (21:19 +0200)
committerGitHub <noreply@github.com>
Sun, 31 Mar 2024 19:19:25 +0000 (21:19 +0200)
Boosts the unit test coverage for the `boschshc` binding in `src/main/java` to 94%.

Signed-off-by: David Pace <dev@davidpace.de>
19 files changed:
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/ScenarioHandler.java
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/console/BoschShcCommandExtensionTest.java
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBatteryPoweredDeviceHandlerTest.java
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBoschSHCDeviceHandlerTest.java
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBoschSHCHandlerTest.java
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandlerTest.java
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandlerWithPowerMeterTest.java
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandlerTest.java
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPollingTest.java
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/ScenarioHandlerTest.java
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/ScenarioTest.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/intrusion/IntrusionDetectionHandlerTest.java
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/shuttercontrol/ShutterControl2HandlerTest.java
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/userdefinedstate/UserStateHandlerTest.java
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/discovery/BridgeDiscoveryParticipantTest.java
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryServiceTest.java
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/userstate/dto/UserStateServiceStateTest.java
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/tests/common/CommonTestUtils.java [new file with mode: 0644]

index 1bae6c86d13e0cb878ae53cdce0ca19987f4961a..0e277048cc1261321699799758cf7408f222e77a 100644 (file)
@@ -657,7 +657,7 @@ public class BridgeHandler extends BaseBridgeHandler {
      *
      * @param e error during long polling
      */
-    private void handleLongPollFailure(Throwable e) {
+    void handleLongPollFailure(Throwable e) {
         logger.warn("Long polling failed, will try to reconnect", e);
         @Nullable
         BoschHttpClient localHttpClient = this.httpClient;
@@ -722,7 +722,7 @@ public class BridgeHandler extends BaseBridgeHandler {
                             return new BoschSHCException("@text/offline.conf-error.invalid-state-id");
                         } else {
                             return new BoschSHCException(String.format(
-                                    "Request for info of user-defines state %s failed with status code %d and error code %s",
+                                    "Request for info of user-defined state %s failed with status code %d and error code %s",
                                     stateId, errorResponse.statusCode, errorResponse.errorCode));
                         }
                     } else {
index 5d3bfe450a21ec96f1b861bc0a84bd9dab1b4743..e69114041d8758aca21dbd582d70b4773471894a 100644 (file)
@@ -96,7 +96,7 @@ public class ScenarioHandler {
         }
     }
 
-    private String prettyLogScenarios(final Scenario[] scenarios) {
+    String prettyLogScenarios(final Scenario[] scenarios) {
         final StringBuilder builder = new StringBuilder();
         builder.append("[");
         for (Scenario scenario : scenarios) {
index 85f7ea3a4315cca826c5f2c569bafa78632be647..425c528d3251ae84e6b80242f75be194a69f0580 100644 (file)
@@ -17,7 +17,12 @@ import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.allOf;
 import static org.hamcrest.Matchers.containsString;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.*;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import java.io.IOException;
 import java.nio.file.Files;
@@ -35,6 +40,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.openhab.binding.boschshc.internal.devices.bridge.BridgeHandler;
@@ -89,6 +96,24 @@ class BoschShcCommandExtensionTest {
         verify(consoleMock, atLeastOnce()).print(any());
     }
 
+    @ParameterizedTest
+    @MethodSource("org.openhab.binding.boschshc.internal.tests.common.CommonTestUtils#getBoschShcAndExecutionAndTimeoutExceptionArguments()")
+    void executeHandleExceptions(Exception exception)
+            throws InterruptedException, BoschSHCException, ExecutionException, TimeoutException {
+        Console console = mock(Console.class);
+        Bridge bridge = mock(Bridge.class);
+        BridgeHandler bridgeHandler = mock(BridgeHandler.class);
+        when(bridgeHandler.getThing()).thenReturn(bridge);
+        when(bridgeHandler.getPublicInformation()).thenThrow(exception);
+        when(bridge.getHandler()).thenReturn(bridgeHandler);
+        List<Thing> things = List.of(bridge);
+        when(thingRegistry.getAll()).thenReturn(things);
+
+        fixture.execute(new String[] { BoschShcCommandExtension.GET_BRIDGEINFO }, console);
+
+        verify(console).print(anyString());
+    }
+
     @Test
     void getCompleter() {
         assertThat(fixture.getCompleter(), is(fixture));
index d01b3721b0593d36687e0d250a900446f6074388..098bd46a8a8cb01985b042ff793fb57756b9ccb8 100644 (file)
@@ -46,13 +46,13 @@ public abstract class AbstractBatteryPoweredDeviceHandlerTest<T extends Abstract
     @BeforeEach
     @Override
     public void beforeEach() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
-        super.beforeEach();
-
         DeviceServiceData deviceServiceData = new DeviceServiceData();
         deviceServiceData.path = "/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel";
         deviceServiceData.id = "BatteryLevel";
         deviceServiceData.deviceId = "hdm:ZigBee:000d6f0004b93361";
-        lenient().when(bridgeHandler.getServiceData(anyString(), anyString())).thenReturn(deviceServiceData);
+        when(getBridgeHandler().getServiceData(anyString(), anyString())).thenReturn(deviceServiceData);
+
+        super.beforeEach();
     }
 
     @Test
@@ -137,10 +137,11 @@ public abstract class AbstractBatteryPoweredDeviceHandlerTest<T extends Abstract
                     "deviceId":"hdm:ZigBee:000d6f0004b93361" }\
                 """);
         getFixture().processUpdate("BatteryLevel", deviceServiceData);
-        verify(getCallback()).stateUpdated(
+        // state is updated twice: via short poll in initialize() and via long poll result in this test
+        verify(getCallback(), times(2)).stateUpdated(
                 new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL),
                 new DecimalType(100));
-        verify(getCallback()).stateUpdated(
+        verify(getCallback(), times(2)).stateUpdated(
                 new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.OFF);
     }
 
@@ -165,19 +166,24 @@ public abstract class AbstractBatteryPoweredDeviceHandlerTest<T extends Abstract
         getFixture().processUpdate("BatteryLevel", deviceServiceData);
         verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL),
                 UnDefType.UNDEF);
-        verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.OFF);
+        // state is updated twice: via short poll in initialize() and via long poll result in this test
+        verify(getCallback(), times(2)).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_LOW_BATTERY),
+                OnOffType.OFF);
     }
 
     @Test
     public void testHandleCommandRefreshBatteryLevelChannel() {
         getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL), RefreshType.REFRESH);
-        verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL),
+        // state is updated twice: via short poll in initialize() and via long poll result in this test
+        verify(getCallback(), times(2)).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL),
                 new DecimalType(100));
     }
 
     @Test
     public void testHandleCommandRefreshLowBatteryChannel() {
         getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), RefreshType.REFRESH);
-        verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.OFF);
+        // state is updated twice: via short poll in initialize() and via long poll result in this test
+        verify(getCallback(), times(2)).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_LOW_BATTERY),
+                OnOffType.OFF);
     }
 }
index 335af59e3da45788f020accb30691f22554adf59..41dd7784d60c5558d7ddcb33995d7dae6f92d58a 100644 (file)
  */
 package org.openhab.binding.boschshc.internal.devices;
 
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
 import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
+import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
 import org.openhab.core.config.core.Configuration;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
 
 /**
  * Abstract unit test implementation for device handlers.
@@ -42,4 +56,27 @@ public abstract class AbstractBoschSHCDeviceHandlerTest<T extends BoschSHCDevice
     }
 
     protected abstract String getDeviceID();
+
+    @Test
+    void initializeInvalidDeviceId() {
+        getFixture().getThing().getConfiguration().remove("id");
+        getFixture().initialize();
+
+        verify(getCallback()).statusUpdated(eq(getThing()),
+                argThat(status -> status.getStatus().equals(ThingStatus.OFFLINE)
+                        && status.getStatusDetail().equals(ThingStatusDetail.CONFIGURATION_ERROR)));
+    }
+
+    @ParameterizedTest
+    @MethodSource("org.openhab.binding.boschshc.internal.tests.common.CommonTestUtils#getExecutionExceptionAndInterruptedExceptionArguments()")
+    void initializeHandleExceptionDuringDeviceInfoRestCall(Exception exception)
+            throws BoschSHCException, InterruptedException, TimeoutException, ExecutionException {
+        when(getBridgeHandler().getDeviceInfo(getDeviceID())).thenThrow(exception);
+
+        getFixture().initialize();
+
+        verify(getCallback()).statusUpdated(eq(getThing()),
+                argThat(status -> status.getStatus().equals(ThingStatus.OFFLINE)
+                        && status.getStatusDetail().equals(ThingStatusDetail.CONFIGURATION_ERROR)));
+    }
 }
index c8efb9c9b31592af115a460340b0986914ab8a1f..1c754d6d32cb187de1433f6b36d70a9a294b0814 100644 (file)
@@ -12,6 +12,9 @@
  */
 package org.openhab.binding.boschshc.internal.devices;
 
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.sameInstance;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
@@ -60,7 +63,7 @@ public abstract class AbstractBoschSHCHandlerTest<T extends BoschSHCHandler> {
 
     private @Mock @NonNullByDefault({}) Bridge bridge;
 
-    protected @Mock @NonNullByDefault({}) BridgeHandler bridgeHandler;
+    private @Mock @NonNullByDefault({}) BridgeHandler bridgeHandler;
 
     private @Mock @NonNullByDefault({}) ThingHandlerCallback callback;
 
@@ -128,8 +131,25 @@ public abstract class AbstractBoschSHCHandlerTest<T extends BoschSHCHandler> {
     }
 
     @Test
-    public void testInitialize() {
+    void testInitialize() {
         ThingStatusInfo expectedStatusInfo = new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
         verify(callback).statusUpdated(same(thing), eq(expectedStatusInfo));
     }
+
+    @Test
+    void testGetBridgeHandler() throws BoschSHCException {
+        assertThat(fixture.getBridgeHandler(), sameInstance(bridgeHandler));
+    }
+
+    @Test
+    void testGetBridgeHandlerThrowExceptionIfBridgeIsNull() throws BoschSHCException {
+        when(callback.getBridge(any())).thenReturn(null);
+        assertThrows(BoschSHCException.class, () -> fixture.getBridgeHandler());
+    }
+
+    @Test
+    void testGetBridgeHandlerThrowExceptionIfBridgeHandlerIsNull() throws BoschSHCException {
+        when(bridge.getHandler()).thenReturn(null);
+        assertThrows(BoschSHCException.class, () -> fixture.getBridgeHandler());
+    }
 }
index 49be02c8286b025615e5b93084343d8df5485c09..a634f0c4c275ef591b33d441c8d2bb0cb682ef4f 100644 (file)
 package org.openhab.binding.boschshc.internal.devices;
 
 import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.same;
-import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeoutException;
@@ -26,12 +28,16 @@ import java.util.concurrent.TimeoutException;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
 import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchState;
 import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitchServiceState;
 import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
 import org.openhab.core.types.RefreshType;
 
 import com.google.gson.JsonElement;
@@ -53,12 +59,12 @@ public abstract class AbstractPowerSwitchHandlerTest<T extends AbstractPowerSwit
     @BeforeEach
     @Override
     public void beforeEach() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
-        super.beforeEach();
-
         PowerSwitchServiceState powerSwitchServiceState = new PowerSwitchServiceState();
         powerSwitchServiceState.switchState = PowerSwitchState.ON;
-        lenient().when(bridgeHandler.getState(anyString(), eq("PowerSwitch"), same(PowerSwitchServiceState.class)))
+        when(getBridgeHandler().getState(anyString(), eq("PowerSwitch"), same(PowerSwitchServiceState.class)))
                 .thenReturn(powerSwitchServiceState);
+
+        super.beforeEach();
     }
 
     @Test
@@ -76,22 +82,58 @@ public abstract class AbstractPowerSwitchHandlerTest<T extends AbstractPowerSwit
         assertSame(PowerSwitchState.OFF, state.switchState);
     }
 
+    @ParameterizedTest
+    @MethodSource("org.openhab.binding.boschshc.internal.tests.common.CommonTestUtils#getExecutionAndTimeoutAndInterruptedExceptionArguments()")
+    public void testHandleCommandPowerSwitchChannelHandleExceptions(Exception e)
+            throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+        when(getBridgeHandler().putState(any(), any(), any())).thenThrow(e);
+
+        getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), OnOffType.ON);
+
+        verify(getCallback()).statusUpdated(same(getThing()),
+                argThat(status -> status.getStatus().equals(ThingStatus.OFFLINE)
+                        && status.getStatusDetail().equals(ThingStatusDetail.COMMUNICATION_ERROR)));
+    }
+
     @Test
     public void testUpdateChannelPowerSwitchState() {
         JsonElement jsonObject = JsonParser
                 .parseString("{\n" + "  \"@type\": \"powerSwitchState\",\n" + "  \"switchState\": \"ON\"\n" + "}");
+
         getFixture().processUpdate("PowerSwitch", jsonObject);
-        verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), OnOffType.ON);
+
+        // state is updated twice: via short poll in initialize() and via long poll result in this test
+        verify(getCallback(), times(2)).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH),
+                OnOffType.ON);
 
         jsonObject = JsonParser
                 .parseString("{\n" + "  \"@type\": \"powerSwitchState\",\n" + "  \"switchState\": \"OFF\"\n" + "}");
+
         getFixture().processUpdate("PowerSwitch", jsonObject);
+
         verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), OnOffType.OFF);
     }
 
     @Test
     public void testHandleCommandRefreshPowerSwitchChannel() {
         getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), RefreshType.REFRESH);
-        verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), OnOffType.ON);
+
+        // state is updated twice: via short poll in initialize() and via long poll result in this test
+        verify(getCallback(), times(2)).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH),
+                OnOffType.ON);
+    }
+
+    @ParameterizedTest
+    @MethodSource("org.openhab.binding.boschshc.internal.tests.common.CommonTestUtils#getBoschShcAndExecutionAndTimeoutAndInterruptedExceptionArguments()")
+    public void testHandleCommandRefreshPowerSwitchChannelHandleExceptions(Exception e)
+            throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+        when(getBridgeHandler().getState(anyString(), eq("PowerSwitch"), same(PowerSwitchServiceState.class)))
+                .thenThrow(e);
+
+        getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), RefreshType.REFRESH);
+
+        verify(getCallback()).statusUpdated(same(getThing()),
+                argThat(status -> status.getStatus().equals(ThingStatus.OFFLINE)
+                        && status.getStatusDetail().equals(ThingStatusDetail.COMMUNICATION_ERROR)));
     }
 }
index 3ce2da12834115d389e511ef0fdd2402c7536539..2fdb8e5e88c9f994d0201793bff0291d4945a496 100644 (file)
@@ -17,6 +17,7 @@ import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.same;
 import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
 import java.util.concurrent.ExecutionException;
@@ -56,13 +57,13 @@ public abstract class AbstractPowerSwitchHandlerWithPowerMeterTest<T extends Abs
 
     @BeforeEach
     public void beforeEach() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
-        super.beforeEach();
-
         PowerMeterServiceState powerMeterServiceState = new PowerMeterServiceState();
         powerMeterServiceState.powerConsumption = 12.34d;
         powerMeterServiceState.energyConsumption = 56.78d;
-        lenient().when(bridgeHandler.getState(anyString(), eq("PowerMeter"), same(PowerMeterServiceState.class)))
+        lenient().when(getBridgeHandler().getState(anyString(), eq("PowerMeter"), same(PowerMeterServiceState.class)))
                 .thenReturn(powerMeterServiceState);
+
+        super.beforeEach();
     }
 
     @Test
@@ -76,13 +77,15 @@ public abstract class AbstractPowerSwitchHandlerWithPowerMeterTest<T extends Abs
                 """);
         getFixture().processUpdate("PowerMeter", jsonObject);
 
-        verify(getCallback()).stateUpdated(eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION)),
-                powerCaptor.capture());
+        // state is updated twice: via short poll in initialize() and via long poll result in this test
+        verify(getCallback(), times(2)).stateUpdated(
+                eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION)), powerCaptor.capture());
         QuantityType<Power> powerValue = powerCaptor.getValue();
         assertEquals(23, powerValue.intValue());
 
-        verify(getCallback()).stateUpdated(eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION)),
-                energyCaptor.capture());
+        // state is updated twice: via short poll in initialize() and via long poll result in this test
+        verify(getCallback(), times(2)).stateUpdated(
+                eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION)), energyCaptor.capture());
         QuantityType<Energy> energyValue = energyCaptor.getValue();
         assertEquals(42, energyValue.intValue());
     }
@@ -91,7 +94,9 @@ public abstract class AbstractPowerSwitchHandlerWithPowerMeterTest<T extends Abs
     public void testHandleCommandRefreshPowerConsumptionChannel() {
         getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION),
                 RefreshType.REFRESH);
-        verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION),
+
+        // state is updated twice: via short poll in initialize() and via refresh command in this test
+        verify(getCallback(), times(2)).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION),
                 new QuantityType<>(12.34d, Units.WATT));
     }
 
@@ -99,7 +104,9 @@ public abstract class AbstractPowerSwitchHandlerWithPowerMeterTest<T extends Abs
     public void testHandleCommandRefreshEnergyConsumptionChannel() {
         getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION),
                 RefreshType.REFRESH);
-        verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION),
+
+        // state is updated twice: via short poll in initialize() and via refresh command in this test
+        verify(getCallback(), times(2)).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION),
                 new QuantityType<>(56.78d, Units.WATT_HOUR));
     }
 }
index 4bfde88740be8326f3e439d3d81a85d7104ec0f0..734551a778427a3682d085a22976dc053596240e 100644 (file)
  */
 package org.openhab.binding.boschshc.internal.devices.bridge;
 
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -22,9 +25,11 @@ import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.contains;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.ArgumentMatchers.same;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
@@ -50,16 +55,23 @@ import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
 import org.mockito.ArgumentCaptor;
+import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
 import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceTest;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Faults;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.PublicInformation;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.Room;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.Scenario;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedState;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedStateTest;
+import org.openhab.binding.boschshc.internal.discovery.ThingDiscoveryService;
 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
 import org.openhab.binding.boschshc.internal.serialization.GsonUtils;
 import org.openhab.binding.boschshc.internal.services.binaryswitch.dto.BinarySwitchServiceState;
@@ -70,15 +82,21 @@ import org.openhab.binding.boschshc.internal.services.intrusion.dto.IntrusionDet
 import org.openhab.binding.boschshc.internal.services.shuttercontact.ShutterContactState;
 import org.openhab.binding.boschshc.internal.services.shuttercontact.dto.ShutterContactServiceState;
 import org.openhab.core.config.core.Configuration;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
 import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
 import org.openhab.core.thing.Thing;
 import org.openhab.core.thing.ThingStatus;
 import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
 import org.openhab.core.thing.binding.ThingHandlerCallback;
 import org.openhab.core.thing.binding.builder.ThingStatusInfoBuilder;
 
 import com.google.gson.JsonElement;
 import com.google.gson.JsonParser;
+import com.google.gson.JsonPrimitive;
 
 /**
  * Unit tests for the {@link BridgeHandler}.
@@ -92,13 +110,9 @@ class BridgeHandlerTest {
     private @NonNullByDefault({}) BridgeHandler fixture;
 
     private @NonNullByDefault({}) BoschHttpClient httpClient;
-
     private @NonNullByDefault({}) ThingHandlerCallback thingHandlerCallback;
-
-    /**
-     * A mocked bridge instance
-     */
     private @NonNullByDefault({}) Bridge thing;
+    private @NonNullByDefault({}) Configuration bridgeConfiguration;
 
     @BeforeAll
     static void beforeAll() throws IOException {
@@ -119,7 +133,7 @@ class BridgeHandlerTest {
         thingHandlerCallback = mock(ThingHandlerCallback.class);
         fixture.setCallback(thingHandlerCallback);
 
-        Configuration bridgeConfiguration = new Configuration();
+        bridgeConfiguration = new Configuration();
         Map<@Nullable String, @Nullable Object> properties = new HashMap<>();
         properties.put("ipAddress", "localhost");
         properties.put("password", "test");
@@ -155,6 +169,19 @@ class BridgeHandlerTest {
         verify(mockRequest).send();
     }
 
+    @Test
+    void postActionWithoutRequestBody() throws InterruptedException, TimeoutException, ExecutionException {
+        String endpoint = "/intrusion/actions/disarm";
+        String url = "https://127.0.0.1:8444/smarthome/intrusion/actions/disarm";
+        when(httpClient.getBoschSmartHomeUrl(endpoint)).thenReturn(url);
+        Request mockRequest = mock(Request.class);
+        when(httpClient.createRequest(anyString(), any(), any())).thenReturn(mockRequest);
+
+        fixture.postAction(endpoint);
+        verify(httpClient).createRequest(eq(url), same(HttpMethod.POST), isNull());
+        verify(mockRequest).send();
+    }
+
     @Test
     void initialAccessHttpClientOffline() {
         fixture.initialAccess(httpClient);
@@ -212,9 +239,31 @@ class BridgeHandlerTest {
         when(httpClient.createRequest(anyString(), same(HttpMethod.POST),
                 argThat((JsonRpcRequest r) -> "RE/longPoll".equals(r.method)))).thenReturn(longPollRequest);
 
+        ThingDiscoveryService thingDiscoveryListener = mock(ThingDiscoveryService.class);
+        fixture.registerDiscoveryListener(thingDiscoveryListener);
+
         fixture.initialAccess(httpClient);
+
         verify(thingHandlerCallback).statusUpdated(any(),
                 eq(ThingStatusInfoBuilder.create(ThingStatus.ONLINE, ThingStatusDetail.NONE).build()));
+        verify(thingDiscoveryListener).doScan();
+    }
+
+    @Test
+    void initialAccessNoBridgeAccess() throws InterruptedException, TimeoutException, ExecutionException {
+        when(httpClient.isOnline()).thenReturn(true);
+        when(httpClient.isAccessPossible()).thenReturn(true);
+        Request request = mock(Request.class);
+        when(httpClient.createRequest(any(), same(HttpMethod.GET))).thenReturn(request);
+        ContentResponse response = mock(ContentResponse.class);
+        when(request.send()).thenReturn(response);
+        when(response.getStatus()).thenReturn(400);
+
+        fixture.initialAccess(httpClient);
+
+        verify(thingHandlerCallback).statusUpdated(same(thing),
+                argThat(status -> status.getStatus().equals(ThingStatus.OFFLINE)
+                        && status.getStatusDetail().equals(ThingStatusDetail.COMMUNICATION_ERROR)));
     }
 
     @Test
@@ -468,6 +517,44 @@ class BridgeHandlerTest {
         assertEquals(stateId, userState.getId());
     }
 
+    @Test
+    void getUserStateInfoErrorCases()
+            throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+        when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
+        when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
+
+        Request request = mock(Request.class);
+        when(request.header(anyString(), anyString())).thenReturn(request);
+        ContentResponse response = mock(ContentResponse.class);
+        when(response.getStatus()).thenReturn(200);
+        when(request.send()).thenReturn(response);
+        when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
+
+        @SuppressWarnings("unchecked")
+        ArgumentCaptor<BiFunction<Integer, String, BoschSHCException>> errorResponseHandlerCaptor = ArgumentCaptor
+                .forClass(BiFunction.class);
+
+        String stateId = "abcdef";
+        when(httpClient.sendRequest(same(request), same(UserDefinedState.class), any(),
+                errorResponseHandlerCaptor.capture())).thenReturn(UserDefinedStateTest.createTestState(stateId));
+
+        fixture.getUserStateInfo(stateId);
+
+        BiFunction<Integer, String, BoschSHCException> errorResponseHandler = errorResponseHandlerCaptor.getValue();
+        Exception e = errorResponseHandler.apply(500,
+                "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"testErrorCode\",\"statusCode\": 500}");
+        assertEquals(
+                "Request for info of user-defined state abcdef failed with status code 500 and error code testErrorCode",
+                e.getMessage());
+
+        e = errorResponseHandler.apply(404,
+                "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"ENTITY_NOT_FOUND\",\"statusCode\": 404}");
+        assertNotNull(e);
+
+        e = errorResponseHandler.apply(500, "");
+        assertEquals("Request for info of user-defined state abcdef failed with status code 500", e.getMessage());
+    }
+
     @Test
     void getUserStates() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
         when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
@@ -650,4 +737,347 @@ class BridgeHandlerTest {
 
         verify(thingHandler).processChildUpdate("hdm:ZigBee:70ac08fffefead2d#3", "PowerSwitch", expectedState);
     }
+
+    @Test
+    void handleLongPollResultScenarioTriggered() {
+        Channel channel = mock(Channel.class);
+        when(thing.getChannel(BoschSHCBindingConstants.CHANNEL_SCENARIO_TRIGGERED)).thenReturn(channel);
+        when(thingHandlerCallback.isChannelLinked(any())).thenReturn(true);
+
+        String json = """
+                {
+                  "result": [{
+                    "@type": "scenarioTriggered",
+                    "name": "My Scenario",
+                    "id": "509bd737-eed0-40b7-8caa-e8686a714399",
+                    "lastTimeTriggered": "1693758693032"
+                  }],
+                  "jsonrpc": "2.0"
+                }
+                """;
+        LongPollResult longPollResult = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(json, LongPollResult.class);
+        assertNotNull(longPollResult);
+
+        fixture.handleLongPollResult(longPollResult);
+
+        verify(thingHandlerCallback).stateUpdated(any(), eq(new StringType("My Scenario")));
+    }
+
+    @Test
+    void handleLongPollResultUserDefinedState() {
+        List<Thing> things = new ArrayList<Thing>();
+        when(thing.getThings()).thenReturn(things);
+
+        Thing thing = mock(Thing.class);
+        things.add(thing);
+
+        BoschSHCHandler thingHandler = mock(BoschSHCHandler.class);
+        when(thing.getHandler()).thenReturn(thingHandler);
+
+        when(thingHandler.getBoschID()).thenReturn("3d8023d6-69ca-4e79-89dd-7090295cefbf");
+
+        String json = """
+                {
+                    "result": [{
+                        "deleted": false,
+                        "@type": "userDefinedState",
+                        "name": "Test State",
+                        "id": "3d8023d6-69ca-4e79-89dd-7090295cefbf",
+                        "state": true
+                    }],
+                    "jsonrpc": "2.0"
+                }
+                """;
+        LongPollResult longPollResult = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(json, LongPollResult.class);
+        assertNotNull(longPollResult);
+
+        fixture.handleLongPollResult(longPollResult);
+
+        JsonElement expectedState = new JsonPrimitive(true);
+
+        verify(thingHandler).processUpdate("3d8023d6-69ca-4e79-89dd-7090295cefbf", expectedState);
+    }
+
+    @Test
+    void handleLongPollFailure() {
+        Throwable e = new RuntimeException("Test exception");
+        fixture.handleLongPollFailure(e);
+
+        ThingStatusInfo expectedStatus = ThingStatusInfoBuilder
+                .create(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE).build();
+        verify(thingHandlerCallback).statusUpdated(thing, expectedStatus);
+    }
+
+    @Test
+    void getDevices() throws InterruptedException, TimeoutException, ExecutionException {
+        Request request = mock(Request.class);
+        when(httpClient.createRequest(any(), eq(HttpMethod.GET))).thenReturn(request);
+        ContentResponse contentResponse = mock(ContentResponse.class);
+        when(request.send()).thenReturn(contentResponse);
+        when(contentResponse.getStatus()).thenReturn(200);
+        String devicesJson = """
+                [
+                    {
+                        "@type": "device",
+                        "rootDeviceId": "64-da-a0-3e-81-0c",
+                        "id": "hdm:ZigBee:0c4314fffea15de7",
+                        "deviceServiceIds": [
+                            "CommunicationQuality",
+                            "PowerMeter",
+                            "PowerSwitch",
+                            "PowerSwitchConfiguration",
+                            "PowerSwitchProgram"
+                        ],
+                        "manufacturer": "BOSCH",
+                        "roomId": "hz_1",
+                        "deviceModel": "PLUG_COMPACT",
+                        "serial": "0C4314FFFE802BE2",
+                        "profile": "LIGHT",
+                        "iconId": "icon_plug_lamp_table",
+                        "name": "My Lamp Plug",
+                        "status": "AVAILABLE",
+                        "childDeviceIds": [],
+                        "supportedProfiles": [
+                            "LIGHT",
+                            "GENERIC",
+                            "HEATING_RCC"
+                        ]
+                    },
+                    {
+                        "@type": "device",
+                        "rootDeviceId": "64-da-a0-3e-81-0c",
+                        "id": "hdm:ZigBee:000d6f0012f13bfa",
+                        "deviceServiceIds": [
+                            "LatestMotion",
+                            "CommunicationQuality",
+                            "WalkTest",
+                            "BatteryLevel",
+                            "MultiLevelSensor",
+                            "DeviceDefect"
+                        ],
+                        "manufacturer": "BOSCH",
+                        "roomId": "hz_5",
+                        "deviceModel": "MD",
+                        "serial": "000D6F0012F0da96",
+                        "profile": "GENERIC",
+                        "name": "My Motion Detector",
+                        "status": "AVAILABLE",
+                        "childDeviceIds": [],
+                        "supportedProfiles": []
+                    }
+                ]
+                """;
+        when(contentResponse.getContentAsString()).thenReturn(devicesJson);
+
+        List<Device> devices = fixture.getDevices();
+
+        assertEquals(2, devices.size());
+
+        Device plugDevice = devices.get(0);
+        assertEquals("hdm:ZigBee:0c4314fffea15de7", plugDevice.id);
+        assertEquals(5, plugDevice.deviceServiceIds.size());
+        assertEquals(0, plugDevice.childDeviceIds.size());
+
+        Device motionDetectorDevice = devices.get(1);
+        assertEquals("hdm:ZigBee:000d6f0012f13bfa", motionDetectorDevice.id);
+        assertEquals(6, motionDetectorDevice.deviceServiceIds.size());
+        assertEquals(0, motionDetectorDevice.childDeviceIds.size());
+    }
+
+    @Test
+    void getDevicesErrorRestResponse() throws InterruptedException, TimeoutException, ExecutionException {
+        Request request = mock(Request.class);
+        when(httpClient.createRequest(any(), eq(HttpMethod.GET))).thenReturn(request);
+        ContentResponse contentResponse = mock(ContentResponse.class);
+        when(request.send()).thenReturn(contentResponse);
+        when(contentResponse.getStatus()).thenReturn(400); // bad request
+
+        List<Device> devices = fixture.getDevices();
+
+        assertThat(devices, hasSize(0));
+    }
+
+    @ParameterizedTest
+    @MethodSource("org.openhab.binding.boschshc.internal.tests.common.CommonTestUtils#getExecutionAndTimeoutExceptionArguments()")
+    void getDevicesHandleExceptions() throws InterruptedException, TimeoutException, ExecutionException {
+        Request request = mock(Request.class);
+        when(httpClient.createRequest(any(), eq(HttpMethod.GET))).thenReturn(request);
+        when(request.send()).thenThrow(new ExecutionException(new RuntimeException("Test Exception")));
+
+        List<Device> devices = fixture.getDevices();
+
+        assertThat(devices, hasSize(0));
+    }
+
+    @Test
+    void getRooms() throws InterruptedException, TimeoutException, ExecutionException {
+        Request request = mock(Request.class);
+        when(httpClient.createRequest(any(), eq(HttpMethod.GET))).thenReturn(request);
+        ContentResponse contentResponse = mock(ContentResponse.class);
+        when(request.send()).thenReturn(contentResponse);
+        when(contentResponse.getStatus()).thenReturn(200);
+        String roomsJson = """
+                [
+                    {
+                        "@type": "room",
+                        "id": "hz_1",
+                        "iconId": "icon_room_living_room",
+                        "name": "Living Room"
+                    },
+                    {
+                        "@type": "room",
+                        "id": "hz_2",
+                        "iconId": "icon_room_dining_room",
+                        "name": "Dining Room"
+                    }
+                ]
+                """;
+        when(contentResponse.getContentAsString()).thenReturn(roomsJson);
+
+        List<Room> rooms = fixture.getRooms();
+
+        assertEquals(2, rooms.size());
+
+        Room livingRoom = rooms.get(0);
+        assertEquals("hz_1", livingRoom.id);
+        assertEquals("Living Room", livingRoom.name);
+
+        Room diningRoom = rooms.get(1);
+        assertEquals("hz_2", diningRoom.id);
+        assertEquals("Dining Room", diningRoom.name);
+    }
+
+    @Test
+    void getRoomsErrorRestResponse() throws InterruptedException, TimeoutException, ExecutionException {
+        Request request = mock(Request.class);
+        when(httpClient.createRequest(any(), eq(HttpMethod.GET))).thenReturn(request);
+        ContentResponse contentResponse = mock(ContentResponse.class);
+        when(request.send()).thenReturn(contentResponse);
+        when(contentResponse.getStatus()).thenReturn(400); // bad request
+
+        List<Room> rooms = fixture.getRooms();
+
+        assertThat(rooms, hasSize(0));
+    }
+
+    @ParameterizedTest
+    @MethodSource("org.openhab.binding.boschshc.internal.tests.common.CommonTestUtils#getExecutionAndTimeoutExceptionArguments()")
+    void getRoomsHandleExceptions() throws InterruptedException, TimeoutException, ExecutionException {
+        Request request = mock(Request.class);
+        when(httpClient.createRequest(any(), eq(HttpMethod.GET))).thenReturn(request);
+        when(request.send()).thenThrow(new ExecutionException(new RuntimeException("Test Exception")));
+
+        List<Room> rooms = fixture.getRooms();
+
+        assertThat(rooms, hasSize(0));
+    }
+
+    @Test
+    void getServices() {
+        assertTrue(fixture.getServices().contains(ThingDiscoveryService.class));
+    }
+
+    @Test
+    void handleCommandIrrelevantChannel() {
+        ChannelUID channelUID = mock(ChannelUID.class);
+        when(channelUID.getId()).thenReturn(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH);
+
+        fixture.handleCommand(channelUID, OnOffType.ON);
+
+        verifyNoInteractions(httpClient);
+    }
+
+    @Test
+    void handleCommandTriggerScenario()
+            throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+        ChannelUID channelUID = mock(ChannelUID.class);
+        when(channelUID.getId()).thenReturn(BoschSHCBindingConstants.CHANNEL_TRIGGER_SCENARIO);
+
+        // required to prevent NPE
+        when(httpClient.sendRequest(any(), eq(Scenario[].class), any(), any())).thenReturn(new Scenario[] {});
+
+        fixture.handleCommand(channelUID, OnOffType.ON);
+
+        verify(httpClient).sendRequest(any(), eq(Scenario[].class), any(), any());
+    }
+
+    @Test
+    void registerDiscoveryListener() {
+        ThingDiscoveryService listener = mock(ThingDiscoveryService.class);
+        assertTrue(fixture.registerDiscoveryListener(listener));
+        assertFalse(fixture.registerDiscoveryListener(listener));
+    }
+
+    @Test
+    void unregisterDiscoveryListener() {
+        assertFalse(fixture.unregisterDiscoveryListener());
+        fixture.registerDiscoveryListener(mock(ThingDiscoveryService.class));
+        assertTrue(fixture.unregisterDiscoveryListener());
+    }
+
+    @Test
+    void initializeNoIpAddress() {
+        bridgeConfiguration.setProperties(new HashMap<String, Object>());
+
+        fixture.initialize();
+
+        ThingStatusInfo expectedStatus = ThingStatusInfoBuilder
+                .create(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR)
+                .withDescription("@text/offline.conf-error-empty-ip").build();
+        verify(thingHandlerCallback).statusUpdated(thing, expectedStatus);
+    }
+
+    @Test
+    void initializeNoPassword() {
+        HashMap<String, Object> properties = new HashMap<String, Object>();
+        properties.put("ipAddress", "localhost");
+        bridgeConfiguration.setProperties(properties);
+
+        fixture.initialize();
+
+        ThingStatusInfo expectedStatus = ThingStatusInfoBuilder
+                .create(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR)
+                .withDescription("@text/offline.conf-error-empty-password").build();
+        verify(thingHandlerCallback).statusUpdated(thing, expectedStatus);
+    }
+
+    @Test
+    void checkBridgeAccess() throws InterruptedException, TimeoutException, ExecutionException {
+        Request request = mock(Request.class);
+        when(httpClient.createRequest(any(), eq(HttpMethod.GET))).thenReturn(request);
+        ContentResponse contentResponse = mock(ContentResponse.class);
+        when(request.send()).thenReturn(contentResponse);
+        when(contentResponse.getStatus()).thenReturn(200);
+
+        assertTrue(fixture.checkBridgeAccess());
+    }
+
+    @Test
+    void checkBridgeAccessRestResponseError() throws InterruptedException, TimeoutException, ExecutionException {
+        Request request = mock(Request.class);
+        when(httpClient.createRequest(any(), eq(HttpMethod.GET))).thenReturn(request);
+        ContentResponse contentResponse = mock(ContentResponse.class);
+        when(request.send()).thenReturn(contentResponse);
+        when(contentResponse.getStatus()).thenReturn(400);
+
+        assertFalse(fixture.checkBridgeAccess());
+    }
+
+    @ParameterizedTest
+    @MethodSource("org.openhab.binding.boschshc.internal.tests.common.CommonTestUtils#getExecutionAndTimeoutExceptionArguments()")
+    void checkBridgeAccessRestException(Exception e) throws InterruptedException, TimeoutException, ExecutionException {
+        Request request = mock(Request.class);
+        when(httpClient.createRequest(any(), eq(HttpMethod.GET))).thenReturn(request);
+        when(request.send()).thenThrow(e);
+
+        assertFalse(fixture.checkBridgeAccess());
+    }
+
+    @Test
+    void getPublicInformation() throws InterruptedException, BoschSHCException, ExecutionException, TimeoutException {
+        fixture.getPublicInformation();
+
+        verify(httpClient).createRequest(any(), same(HttpMethod.GET));
+        verify(httpClient).sendRequest(any(), same(PublicInformation.class), any(), isNull());
+    }
 }
index 596ea19dad07a67d0bcd2872fa1abc7859b54836..6ae484982520b473a79c12662468d9df4e8d448b 100644 (file)
  */
 package org.openhab.binding.boschshc.internal.devices.bridge;
 
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
@@ -43,6 +55,8 @@ import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
@@ -53,6 +67,7 @@ import org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedState;
 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
 import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException;
+import org.openhab.binding.boschshc.internal.tests.common.CommonTestUtils;
 
 import com.google.gson.JsonObject;
 import com.google.gson.JsonSyntaxException;
@@ -332,18 +347,21 @@ class LongPollingTest {
         assertTrue(longPollResultItem.isState());
     }
 
-    @Test
-    void startSubscriptionFailure()
+    @ParameterizedTest
+    @MethodSource("org.openhab.binding.boschshc.internal.tests.common.CommonTestUtils#getBoschShcAndExecutionAndTimeoutAndInterruptedExceptionArguments()")
+    void startSubscriptionFailureHandleExceptions(Exception exception)
             throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
-        when(httpClient.sendRequest(any(), same(SubscribeResult.class), any(), any()))
-                .thenThrow(new ExecutionException("Subscription failed.", null));
+        when(httpClient.sendRequest(any(), same(SubscribeResult.class), any(), any())).thenThrow(exception);
 
         LongPollingFailedException e = assertThrows(LongPollingFailedException.class, () -> fixture.start(httpClient));
-        assertTrue(e.getMessage().contains("Subscription failed."));
+        assertThat(e.getCause(), instanceOf(exception.getClass()));
+        assertThat(e.getMessage(), containsString(CommonTestUtils.TEST_EXCEPTION_MESSAGE));
     }
 
-    @Test
-    void startLongPollFailure() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+    @ParameterizedTest
+    @MethodSource("org.openhab.binding.boschshc.internal.tests.common.CommonTestUtils#getExceutionExceptionAndRuntimeExceptionArguments()")
+    void startLongPollFailure(Exception exception)
+            throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
         when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
 
         Request request = mock(Request.class);
@@ -364,7 +382,6 @@ class LongPollingTest {
         BufferingResponseListener bufferingResponseListener = (BufferingResponseListener) completeListener.getValue();
 
         Result result = mock(Result.class);
-        ExecutionException exception = new ExecutionException("test exception", null);
         when(result.getFailure()).thenReturn(exception);
         bufferingResponseListener.onComplete(result);
 
index 5ca0f3b8e52430a64d69e9b9f09bf1c3b00bc87c..044b635833725e6ddab534c3da64be6c5309566a 100644 (file)
  */
 package org.openhab.binding.boschshc.internal.devices.bridge;
 
-import static org.mockito.Mockito.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import java.util.List;
 import java.util.UUID;
@@ -24,10 +30,12 @@ import org.eclipse.jetty.client.api.ContentResponse;
 import org.eclipse.jetty.client.api.Request;
 import org.eclipse.jetty.http.HttpMethod;
 import org.eclipse.jetty.http.HttpStatus;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Scenario;
 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
@@ -61,11 +69,19 @@ class ScenarioHandlerTest {
                 .toArray(Exception[]::new);
     }
 
+    private @NonNullByDefault({}) ScenarioHandler fixture;
+
+    private @NonNullByDefault({}) @Mock BoschHttpClient httpClient;
+    private @NonNullByDefault({}) @Mock Request request;
+
+    @BeforeEach
+    void beforeEach() {
+        fixture = new ScenarioHandler();
+    }
+
     @Test
     void triggerScenarioShouldSendPOSTToBoschAPI() throws Exception {
         // GIVEN
-        final var httpClient = mock(BoschHttpClient.class);
-        final var request = mock(Request.class);
         final var contentResponse = mock(ContentResponse.class);
         when(httpClient.getBoschSmartHomeUrl(anyString())).thenReturn("http://localhost/smartHome/scenarios")
                 .thenReturn("http://localhost/smartHome/scenarios/1234/triggers");
@@ -74,10 +90,8 @@ class ScenarioHandlerTest {
         when(request.send()).thenReturn(contentResponse);
         when(contentResponse.getStatus()).thenReturn(HttpStatus.OK_200);
 
-        final var handler = new ScenarioHandler();
-
         // WHEN
-        handler.triggerScenario(httpClient, "Scenario 1");
+        fixture.triggerScenario(httpClient, "Scenario 1");
 
         // THEN
         verify(httpClient).getBoschSmartHomeUrl("scenarios");
@@ -85,19 +99,15 @@ class ScenarioHandlerTest {
     }
 
     @Test
-    void triggerScenarioShouldNoSendPOSTToScenarioNameDoesNotExist() throws Exception {
+    void triggerScenarioShouldNotSendPOSTToScenarioNameDoesNotExist() throws Exception {
         // GIVEN
-        final var httpClient = mock(BoschHttpClient.class);
-        final var request = mock(Request.class);
         when(httpClient.getBoschSmartHomeUrl(anyString())).thenReturn("http://localhost/smartHome/scenarios")
                 .thenReturn("http://localhost/smartHome/scenarios/1234/triggers");
         when(httpClient.createRequest(anyString(), any(HttpMethod.class))).thenReturn(request).thenReturn(request);
         when(httpClient.sendRequest(any(Request.class), any(), any(), any())).thenReturn(existingScenarios);
 
-        final var handler = new ScenarioHandler();
-
         // WHEN
-        handler.triggerScenario(httpClient, "not existing Scenario");
+        fixture.triggerScenario(httpClient, "not existing Scenario");
 
         // THEN
         verify(httpClient).getBoschSmartHomeUrl("scenarios");
@@ -108,17 +118,13 @@ class ScenarioHandlerTest {
     @MethodSource("exceptionData")
     void triggerScenarioShouldNotPanicIfBoschAPIThrowsException(final Exception exception) throws Exception {
         // GIVEN
-        final var httpClient = mock(BoschHttpClient.class);
-        final var request = mock(Request.class);
         when(httpClient.getBoschSmartHomeUrl(anyString())).thenReturn("http://localhost/smartHome/scenarios")
                 .thenReturn("http://localhost/smartHome/scenarios/1234/triggers");
         when(httpClient.createRequest(anyString(), any(HttpMethod.class))).thenReturn(request);
         when(httpClient.sendRequest(any(Request.class), any(), any(), any())).thenThrow(exception);
 
-        final var handler = new ScenarioHandler();
-
         // WHEN
-        handler.triggerScenario(httpClient, "Scenario 1");
+        fixture.triggerScenario(httpClient, "Scenario 1");
 
         // THEN
         verify(httpClient).getBoschSmartHomeUrl("scenarios");
@@ -128,8 +134,6 @@ class ScenarioHandlerTest {
     @Test
     void triggerScenarioShouldNotPanicIfPOSTIsNotSuccessful() throws Exception {
         // GIVEN
-        final var httpClient = mock(BoschHttpClient.class);
-        final var request = mock(Request.class);
         final var contentResponse = mock(ContentResponse.class);
         when(httpClient.getBoschSmartHomeUrl(anyString())).thenReturn("http://localhost/smartHome/scenarios")
                 .thenReturn("http://localhost/smartHome/scenarios/1234/triggers");
@@ -138,10 +142,8 @@ class ScenarioHandlerTest {
         when(request.send()).thenReturn(contentResponse);
         when(contentResponse.getStatus()).thenReturn(HttpStatus.METHOD_NOT_ALLOWED_405);
 
-        final var handler = new ScenarioHandler();
-
         // WHEN
-        handler.triggerScenario(httpClient, "Scenario 1");
+        fixture.triggerScenario(httpClient, "Scenario 1");
 
         // THEN
         verify(httpClient).getBoschSmartHomeUrl("scenarios");
@@ -152,21 +154,27 @@ class ScenarioHandlerTest {
     @MethodSource("httpExceptionData")
     void triggerScenarioShouldNotPanicIfPOSTThrowsException(final Exception exception) throws Exception {
         // GIVEN
-        final var httpClient = mock(BoschHttpClient.class);
-        final var request = mock(Request.class);
         when(httpClient.getBoschSmartHomeUrl(anyString())).thenReturn("http://localhost/smartHome/scenarios")
                 .thenReturn("http://localhost/smartHome/scenarios/1234/triggers");
         when(httpClient.createRequest(anyString(), any(HttpMethod.class))).thenReturn(request).thenReturn(request);
         when(httpClient.sendRequest(any(Request.class), any(), any(), any())).thenReturn(existingScenarios);
         when(request.send()).thenThrow(exception);
 
-        final var handler = new ScenarioHandler();
-
         // WHEN
-        handler.triggerScenario(httpClient, "Scenario 1");
+        fixture.triggerScenario(httpClient, "Scenario 1");
 
         // THEN
         verify(httpClient).getBoschSmartHomeUrl("scenarios");
         verify(request).send();
     }
+
+    @Test
+    void prettyLogScenarios() {
+        Scenario scenario1 = Scenario.createScenario("id1", "Scenario 1", "1708619045411");
+        Scenario scenario2 = Scenario.createScenario("id2", "Scenario 2", "1708619065445");
+        assertEquals(
+                "[\n" + "  Scenario{name='Scenario 1', id='id1', lastTimeTriggered='1708619045411'}\n"
+                        + "  Scenario{name='Scenario 2', id='id2', lastTimeTriggered='1708619065445'}\n" + "]",
+                fixture.prettyLogScenarios(new Scenario[] { scenario1, scenario2 }));
+    }
 }
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/ScenarioTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/ScenarioTest.java
new file mode 100644 (file)
index 0000000..ebf574d
--- /dev/null
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * 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.openhab.binding.boschshc.internal.devices.bridge.dto;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link Scenario}.
+ * 
+ * @author David Pace - Initial contribution
+ *
+ */
+class ScenarioTest {
+
+    private Scenario fixture;
+
+    @BeforeEach
+    protected void setUp() throws Exception {
+        fixture = Scenario.createScenario("abc", "My Scenario", "1708845918493");
+    }
+
+    @Test
+    void isValid() {
+        assertTrue(Scenario.isValid(new Scenario[] { fixture }));
+        assertFalse(Scenario.isValid(new Scenario[] { fixture, new Scenario() }));
+    }
+
+    @Test
+    void testToString() {
+        assertEquals("Scenario{name='My Scenario', id='abc', lastTimeTriggered='1708845918493'}", fixture.toString());
+    }
+}
index 3a3152e91ef12fc5f2b0939f86e5949f57679dca..36dca5c7ace8e75d710ccc7aecdaa6976a1742f0 100644 (file)
 package org.openhab.binding.boschshc.internal.devices.intrusion;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeoutException;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.openhab.binding.boschshc.internal.devices.AbstractBoschSHCHandlerTest;
@@ -29,6 +35,8 @@ import org.openhab.binding.boschshc.internal.services.intrusion.actions.arm.dto.
 import org.openhab.core.library.types.OnOffType;
 import org.openhab.core.library.types.StringType;
 import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
 import org.openhab.core.thing.ThingTypeUID;
 
 import com.google.gson.JsonElement;
@@ -64,6 +72,20 @@ class IntrusionDetectionHandlerTest extends AbstractBoschSHCHandlerTest<Intrusio
         assertEquals("0", armRequest.profileId);
     }
 
+    @ParameterizedTest
+    @MethodSource("org.openhab.binding.boschshc.internal.tests.common.CommonTestUtils#getExecutionAndTimeoutAndInterruptedExceptionArguments()")
+    void testHandleCommandArmActionHandleExceptions(Exception e)
+            throws InterruptedException, TimeoutException, ExecutionException {
+        when(getBridgeHandler().postAction(any(), any())).thenThrow(e);
+
+        getFixture().handleCommand(new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_ARM_ACTION),
+                new StringType("0"));
+
+        verify(getBridgeHandler()).postAction(eq("intrusion/actions/arm"), armActionRequestCaptor.capture());
+        ArmActionRequest armRequest = armActionRequestCaptor.getValue();
+        assertEquals("0", armRequest.profileId);
+    }
+
     @Test
     void testHandleCommandDisarmAction() throws InterruptedException, TimeoutException, ExecutionException {
         getFixture().handleCommand(new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_DISARM_ACTION),
@@ -71,6 +93,20 @@ class IntrusionDetectionHandlerTest extends AbstractBoschSHCHandlerTest<Intrusio
         verify(getBridgeHandler()).postAction("intrusion/actions/disarm");
     }
 
+    @ParameterizedTest
+    @MethodSource("org.openhab.binding.boschshc.internal.tests.common.CommonTestUtils#getExecutionAndTimeoutAndInterruptedExceptionArguments()")
+    void testHandleCommandDisarmActionHandleExceptions(Exception e)
+            throws InterruptedException, TimeoutException, ExecutionException {
+        when(getBridgeHandler().postAction(any())).thenThrow(e);
+
+        getFixture().handleCommand(new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_DISARM_ACTION),
+                OnOffType.ON);
+
+        verify(getCallback()).statusUpdated(same(getThing()),
+                argThat(status -> status.getStatus().equals(ThingStatus.OFFLINE)
+                        && status.getStatusDetail().equals(ThingStatusDetail.COMMUNICATION_ERROR)));
+    }
+
     @Test
     void testHandleCommandMuteAction() throws InterruptedException, TimeoutException, ExecutionException {
         getFixture().handleCommand(new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_MUTE_ACTION),
index b5a21927b9ed0fb72ca0c77d762afa32042070fe..1d5e873f05f79c63a9cd7e04d435a9622cb91a03 100644 (file)
@@ -15,6 +15,7 @@ package org.openhab.binding.boschshc.internal.devices.shuttercontrol;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
 import java.util.concurrent.ExecutionException;
@@ -140,4 +141,14 @@ class ShutterControl2HandlerTest extends ShutterControlHandlerTest {
         ChildProtectionServiceState state = childProtectionServiceStateCaptor.getValue();
         assertTrue(state.childLockActive);
     }
+
+    @Test
+    void testHandleCommandChildProtectionInvalidCommand()
+            throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+        getFixture().handleCommand(
+                new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION),
+                DecimalType.ZERO);
+        verify(getBridgeHandler(), times(0)).putState(eq(getDeviceID()), eq("ChildProtection"),
+                childProtectionServiceStateCaptor.capture());
+    }
 }
index 1b489f898ab787e8d640d64bc4a6f7d1de04aa35..f2bbd46d84bfb103216566db067ee9211b796300 100644 (file)
@@ -17,22 +17,19 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.same;
-import static org.mockito.Mockito.lenient;
-import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import java.util.Map;
 import java.util.UUID;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeoutException;
-import java.util.stream.Stream;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
 import org.mockito.ArgumentCaptor;
 import org.openhab.binding.boschshc.internal.devices.AbstractBoschSHCHandlerTest;
@@ -44,9 +41,7 @@ import org.openhab.core.library.types.OnOffType;
 import org.openhab.core.thing.ChannelUID;
 import org.openhab.core.thing.ThingStatus;
 import org.openhab.core.thing.ThingStatusDetail;
-import org.openhab.core.thing.ThingStatusInfo;
 import org.openhab.core.thing.ThingTypeUID;
-import org.openhab.core.thing.ThingUID;
 
 /**
  * Unit tests for UserStateHandlerTest
@@ -94,25 +89,41 @@ class UserStateHandlerTest extends AbstractBoschSHCHandlerTest<UserStateHandler>
     }
 
     @ParameterizedTest()
-    @MethodSource("provideExceptions")
-    void testHandleCommandSetStateUpdatesThingStatusOnException(Exception mockException)
+    @MethodSource("org.openhab.binding.boschshc.internal.tests.common.CommonTestUtils#getExecutionExceptionAndInterruptedExceptionArguments()")
+    void testHandleCommandSetStateUpdatesThingStatusOnException(Exception exception)
             throws InterruptedException, TimeoutException, ExecutionException {
-        reset(getCallback());
-        lenient().when(getBridgeHandler().putState(anyString(), anyString(), any(UserStateServiceState.class)))
-                .thenThrow(mockException);
+        when(getBridgeHandler().putState(anyString(), anyString(), any(UserStateServiceState.class)))
+                .thenThrow(exception);
         var channel = new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_USER_DEFINED_STATE);
+
         getFixture().handleCommand(channel, OnOffType.ON);
 
-        verify(getCallback()).getBridge(any(ThingUID.class));
+        verify(getCallback()).statusUpdated(same(getThing()),
+                argThat(status -> status.getStatus().equals(ThingStatus.OFFLINE)
+                        && status.getStatusDetail().equals(ThingStatusDetail.COMMUNICATION_ERROR)));
+    }
+
+    @Test
+    void initializeWithoutId() {
+        when(getThing().getConfiguration()).thenReturn(new Configuration());
 
-        ThingStatusInfo expectedStatusInfo = new ThingStatusInfo(ThingStatus.OFFLINE,
-                ThingStatusDetail.COMMUNICATION_ERROR,
-                String.format("Error while putting user-defined state for %s", channel.getThingUID().getId()));
-        verify(getCallback()).statusUpdated(same(getThing()), eq(expectedStatusInfo));
+        getFixture().initialize();
+
+        verify(getCallback()).statusUpdated(same(getThing()),
+                argThat(status -> status.getStatus().equals(ThingStatus.OFFLINE)
+                        && status.getStatusDetail().equals(ThingStatusDetail.CONFIGURATION_ERROR)));
     }
 
-    private static Stream<Arguments> provideExceptions() {
-        return Stream.of(Arguments.of(new TimeoutException("test exception")),
-                Arguments.of(new InterruptedException("test exception")));
+    @ParameterizedTest
+    @MethodSource("org.openhab.binding.boschshc.internal.tests.common.CommonTestUtils#getBoschShcAndExecutionAndTimeoutAndInterruptedExceptionArguments()")
+    void initializeHandleExceptions(Exception e)
+            throws BoschSHCException, InterruptedException, TimeoutException, ExecutionException {
+        when(getBridgeHandler().getUserStateInfo(anyString())).thenThrow(e);
+
+        getFixture().initialize();
+
+        verify(getCallback()).statusUpdated(same(getThing()),
+                argThat(status -> status.getStatus().equals(ThingStatus.OFFLINE)
+                        && status.getStatusDetail().equals(ThingStatusDetail.CONFIGURATION_ERROR)));
     }
 }
index c98d24142f07ecf04f1c02a9f0e503e4fff5c188..e145d9e7cd15017cfa585c43eb00f30170351beb 100644 (file)
@@ -22,7 +22,9 @@ import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import java.net.ConnectException;
@@ -48,6 +50,7 @@ import org.mockito.quality.Strictness;
 import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.PublicInformation;
 import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.io.net.http.HttpClientFactory;
 import org.openhab.core.thing.ThingUID;
 
 /**
@@ -128,6 +131,7 @@ class BridgeDiscoveryParticipantTest {
     @Test
     void testCreateResult() throws Exception {
         DiscoveryResult result = fixture.createResult(shcBridge);
+
         assertNotNull(result);
         assertThat(result.getBindingId(), is(BoschSHCBindingConstants.BINDING_ID));
         assertThat(result.getThingUID().getId(), is("192-168-0-123"));
@@ -138,19 +142,23 @@ class BridgeDiscoveryParticipantTest {
     @Test
     void testCreateResultOtherDevice() throws Exception {
         DiscoveryResult result = fixture.createResult(otherDevice);
+
         assertNull(result);
     }
 
     @Test
     void testCreateResultNoIPAddress() throws Exception {
         when(shcBridge.getHostAddresses()).thenReturn(new String[] { "" });
+
         DiscoveryResult result = fixture.createResult(shcBridge);
+
         assertNull(result);
     }
 
     @Test
     void testGetThingUID() throws Exception {
         ThingUID thingUID = fixture.getThingUID(shcBridge);
+
         assertNotNull(thingUID);
         assertThat(thingUID.getBindingId(), is(BoschSHCBindingConstants.BINDING_ID));
         assertThat(thingUID.getId(), is("192-168-0-123"));
@@ -165,6 +173,7 @@ class BridgeDiscoveryParticipantTest {
     void testGetBridgeAddress() throws Exception {
         @Nullable
         PublicInformation bridgeInformation = fixture.discoverBridge("192.168.0.123");
+
         assertThat(bridgeInformation, not(nullValue()));
         assertThat(bridgeInformation.shcIpAddress, is("192.168.0.123"));
     }
@@ -178,6 +187,7 @@ class BridgeDiscoveryParticipantTest {
     void testGetPublicInformationFromPossibleBridgeAddress() throws Exception {
         @Nullable
         PublicInformation bridgeInformation = fixture.getPublicInformationFromPossibleBridgeAddress("192.168.0.123");
+
         assertThat(bridgeInformation, not(nullValue()));
         assertThat(bridgeInformation.shcIpAddress, is("192.168.0.123"));
     }
@@ -187,6 +197,7 @@ class BridgeDiscoveryParticipantTest {
         when(contentResponse.getContentAsString()).thenReturn("{\"nothing\":\"useful\"}");
 
         fixture = new BridgeDiscoveryParticipant(mockHttpClient);
+
         assertThat(fixture.getPublicInformationFromPossibleBridgeAddress("192.168.0.123"), is(nullValue()));
     }
 
@@ -195,6 +206,7 @@ class BridgeDiscoveryParticipantTest {
         when(contentResponse.getStatus()).thenReturn(HttpStatus.BAD_REQUEST_400);
 
         fixture = new BridgeDiscoveryParticipant(mockHttpClient);
+
         assertThat(fixture.getPublicInformationFromPossibleBridgeAddress("192.168.0.123"), is(nullValue()));
     }
 
@@ -207,4 +219,13 @@ class BridgeDiscoveryParticipantTest {
         PublicInformation result2 = fixture.getOrComputePublicInformation("192.168.0.123");
         assertSame(result, result2);
     }
+
+    @Test
+    void testPublicConstructor() {
+        HttpClientFactory httpClientFactory = mock(HttpClientFactory.class);
+
+        fixture = new BridgeDiscoveryParticipant(httpClientFactory);
+
+        verify(httpClientFactory).createHttpClient(eq(BoschSHCBindingConstants.BINDING_ID), any());
+    }
 }
index 92ce5dd4a1310f1a4764e2ade67195cba4a51c2d..cc5f5d58b4e41f62e62666836af8a85fd4655b5f 100644 (file)
@@ -25,6 +25,7 @@ import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import java.util.ArrayList;
+import java.util.List;
 import java.util.UUID;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -77,10 +78,35 @@ class ThingDiscoveryServiceTest {
         when(bridgeHandler.getThing()).thenReturn(mockBridge);
     }
 
+    @Test
+    void initialize() {
+        fixture.initialize();
+        verify(bridgeHandler).registerDiscoveryListener(fixture);
+    }
+
     @Test
     void testStartScan() throws InterruptedException {
         mockBridgeCalls();
 
+        Device device = new Device();
+        device.name = "My Smart Plug";
+        device.deviceModel = "PSM";
+        device.id = "hdm:HomeMaticIP:3014F711A00004953859F31B";
+        device.deviceServiceIds = List.of("PowerMeter", "PowerSwitch", "PowerSwitchProgram", "Routing");
+
+        List<Device> devices = new ArrayList<>();
+        devices.add(device);
+        when(bridgeHandler.getDevices()).thenReturn(devices);
+
+        UserDefinedState userDefinedState = new UserDefinedState();
+        userDefinedState.setName("My State");
+        userDefinedState.setId("23d34fa6-382a-444d-8aae-89c706e22158");
+        userDefinedState.setState(true);
+
+        List<UserDefinedState> userDefinedStates = new ArrayList<>();
+        userDefinedStates.add(userDefinedState);
+        when(bridgeHandler.getUserStates()).thenReturn(userDefinedStates);
+
         fixture.activate();
         fixture.startScan();
 
@@ -268,6 +294,15 @@ class ThingDiscoveryServiceTest {
         verify(discoveryListener, times(2)).thingDiscovered(any(), any());
     }
 
+    @Test
+    void dispose() {
+        Bridge thing = mock(Bridge.class);
+        when(thing.getUID()).thenReturn(new ThingUID(BoschSHCBindingConstants.THING_TYPE_SHC, "shc123456"));
+        when(bridgeHandler.getThing()).thenReturn(thing);
+        fixture.dispose();
+        verify(bridgeHandler).unregisterDiscoveryListener();
+    }
+
     @Test
     void getThingTypeUIDLightControl2ChildDevice() {
         Device device = new Device();
index 7d21e34eafe39254497576e196e1bdce94726a60..f2e70ad55fc1e97889699b255b71bd8649ec339a 100644 (file)
@@ -71,4 +71,9 @@ class UserStateServiceStateTest {
         subject.setState(true);
         assertEquals(OnOffType.ON, subject.toOnOffType());
     }
+
+    @Test
+    void testToString() {
+        assertEquals("UserStateServiceState{state=false, type='userdefinedstates'}", subject.toString());
+    }
 }
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/tests/common/CommonTestUtils.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/tests/common/CommonTestUtils.java
new file mode 100644 (file)
index 0000000..8298f9e
--- /dev/null
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * 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.openhab.binding.boschshc.internal.tests.common;
+
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
+
+/**
+ * Common utilities used in unit tests.
+ * 
+ * @author David Pace - Initial contribution
+ *
+ */
+@NonNullByDefault
+public final class CommonTestUtils {
+
+    public static final String TEST_EXCEPTION_MESSAGE = "Test exception";
+
+    private CommonTestUtils() {
+        // Utility Class
+    }
+
+    public static List<Exception> getExecutionExceptionAndInterruptedExceptionArguments() {
+        return List.of(new ExecutionException(TEST_EXCEPTION_MESSAGE, null),
+                new InterruptedException(TEST_EXCEPTION_MESSAGE));
+    }
+
+    public static List<Exception> getExceutionExceptionAndRuntimeExceptionArguments() {
+        return List.of(new ExecutionException(TEST_EXCEPTION_MESSAGE, null),
+                new RuntimeException(TEST_EXCEPTION_MESSAGE));
+    }
+
+    public static List<Exception> getBoschShcAndExecutionAndTimeoutExceptionArguments() {
+        return List.of(new BoschSHCException(TEST_EXCEPTION_MESSAGE),
+                new ExecutionException(TEST_EXCEPTION_MESSAGE, null), new TimeoutException(TEST_EXCEPTION_MESSAGE));
+    }
+
+    public static List<Exception> getBoschShcAndExecutionAndTimeoutAndInterruptedExceptionArguments() {
+        return List.of(new BoschSHCException(TEST_EXCEPTION_MESSAGE),
+                new ExecutionException(TEST_EXCEPTION_MESSAGE, null), new TimeoutException(TEST_EXCEPTION_MESSAGE),
+                new InterruptedException(TEST_EXCEPTION_MESSAGE));
+    }
+
+    public static List<Exception> getExecutionAndTimeoutAndInterruptedExceptionArguments() {
+        return List.of(new ExecutionException(TEST_EXCEPTION_MESSAGE, null),
+                new TimeoutException(TEST_EXCEPTION_MESSAGE), new InterruptedException(TEST_EXCEPTION_MESSAGE));
+    }
+
+    public static List<Exception> getExecutionAndTimeoutExceptionArguments() {
+        return List.of(new ExecutionException(TEST_EXCEPTION_MESSAGE, null),
+                new TimeoutException(TEST_EXCEPTION_MESSAGE));
+    }
+}