]> git.basschouten.com Git - openhab-addons.git/commitdiff
[boschshc] Fix NPE during deserialization, make long polling more robust (#17190)
authorDavid Pace <dev@davidpace.de>
Tue, 13 Aug 2024 18:20:51 +0000 (20:20 +0200)
committerGitHub <noreply@github.com>
Tue, 13 Aug 2024 18:20:51 +0000 (20:20 +0200)
* Fix NPE while deserializing service data JSON objects
* Catch exceptions while handling long poll results
* Correct @type name for user-defined states
* Add unit tests and enhance existing unit tests

Signed-off-by: David Pace <dev@davidpace.de>
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPolling.java
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/UserDefinedState.java
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/serialization/BoschServiceDataDeserializer.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/dto/UserDefinedStateTest.java
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/serialization/BoschServiceDataDeserializerTest.java
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/dto/BoschSHCServiceStateTest.java

index 0cc9a13f24c0dae2bc3abd37c2af1feced3ff76e..5c307002ee40ef1c8cc3b4c2a000795ca318d2cb 100644 (file)
@@ -211,7 +211,7 @@ public class LongPolling {
      * @param subscriptionId Id of subscription the response is for
      * @param content Content of the response
      */
-    private void handleLongPollResponse(BoschHttpClient httpClient, String subscriptionId, @Nullable String content) {
+    void handleLongPollResponse(BoschHttpClient httpClient, String subscriptionId, @Nullable String content) {
         logger.debug("Long poll response: {}", content);
 
         try {
@@ -239,6 +239,10 @@ public class LongPolling {
             this.handleFailure.accept(
                     new LongPollingFailedException("Could not deserialize long poll response: '" + content + "'", e));
             return;
+        } catch (Exception e) {
+            this.handleFailure.accept(
+                    new LongPollingFailedException("Error while handling long poll response: '" + content + "'", e));
+            return;
         }
 
         // Execute next run
index ff1615b4801a95cbe34d3590a1a4048e24fdb455..6a0464645fbf925d285b6a35fcdc28a0b7ad0463 100644 (file)
@@ -37,7 +37,7 @@ public class UserDefinedState extends BoschSHCServiceState {
     private boolean state;
 
     public UserDefinedState() {
-        super("UserDefinedState");
+        super("userDefinedState");
     }
 
     public String getId() {
index 41b02cfbfb8d299f0964935e8ba744f466f81f23..ffaa617c82036758caa29fbfdb662ef9b6a1fcde 100644 (file)
@@ -32,6 +32,7 @@ import com.google.gson.JsonParseException;
  * Utility class for JSON deserialization of device data and triggered scenarios using Google Gson.
  *
  * @author Patrick Gell - Initial contribution
+ * @author David Pace - Fixed NPEs and simplified code, added sanity check
  *
  */
 @NonNullByDefault
@@ -42,36 +43,18 @@ public class BoschServiceDataDeserializer implements JsonDeserializer<BoschSHCSe
     public BoschSHCServiceState deserialize(JsonElement jsonElement, Type type,
             JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
         JsonObject jsonObject = jsonElement.getAsJsonObject();
-        JsonElement dataType = jsonObject.get("@type");
-        switch (dataType.getAsString()) {
-            case "DeviceServiceData" -> {
-                var deviceServiceData = new DeviceServiceData();
-                deviceServiceData.deviceId = jsonObject.get("deviceId").getAsString();
-                deviceServiceData.state = jsonObject.get("state");
-                deviceServiceData.id = jsonObject.get("id").getAsString();
-                deviceServiceData.path = jsonObject.get("path").getAsString();
-                return deviceServiceData;
-            }
-            case "scenarioTriggered" -> {
-                var scenario = new Scenario();
-                scenario.id = jsonObject.get("id").getAsString();
-                scenario.name = jsonObject.get("name").getAsString();
-                scenario.lastTimeTriggered = jsonObject.get("lastTimeTriggered").getAsString();
-                return scenario;
-            }
-            case "userDefinedState" -> {
-                var state = new UserDefinedState();
-                state.setId(jsonObject.get("id").getAsString());
-                state.setName(jsonObject.get("name").getAsString());
-                state.setState(jsonObject.get("state").getAsBoolean());
-                return state;
-            }
-            case "message" -> {
-                return GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(jsonElement, Message.class);
-            }
-            default -> {
-                return new BoschSHCServiceState(dataType.getAsString());
-            }
+        JsonElement dataTypeElement = jsonObject.get("@type");
+        if (dataTypeElement == null) {
+            throw new IllegalArgumentException("Received a service state without a @type property: " + jsonObject);
         }
+
+        String dataType = dataTypeElement.getAsString();
+        return switch (dataType) {
+            case "DeviceServiceData" -> GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(jsonObject, DeviceServiceData.class);
+            case "scenarioTriggered" -> GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(jsonObject, Scenario.class);
+            case "userDefinedState" -> GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(jsonObject, UserDefinedState.class);
+            case "message" -> GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(jsonElement, Message.class);
+            default -> new BoschSHCServiceState(dataType);
+        };
     }
 }
index 6ae484982520b473a79c12662468d9df4e8d448b..4fd17ada2089adb010449469abd94f01f23b17f7 100644 (file)
@@ -14,6 +14,7 @@ package org.openhab.binding.boschshc.internal.devices.bridge;
 
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -24,6 +25,7 @@ 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.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -461,10 +463,35 @@ class LongPollingTest {
         ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
         verify(failureHandler).accept(throwableCaptor.capture());
         Throwable t = throwableCaptor.getValue();
-        assertEquals(
-                "Could not deserialize long poll response: '<HTML><HEAD><TITLE>400</TITLE></HEAD><BODY><H1>400 Unsupported HTTP Protocol Version: /remote/json-rpcHTTP/1.1</H1></BODY></HTML>'",
-                t.getMessage());
-        assertTrue(t.getCause() instanceof JsonSyntaxException);
+        assertThat(t.getMessage(), is(
+                "Could not deserialize long poll response: '<HTML><HEAD><TITLE>400</TITLE></HEAD><BODY><H1>400 Unsupported HTTP Protocol Version: /remote/json-rpcHTTP/1.1</H1></BODY></HTML>'"));
+        assertThat(t.getCause(), instanceOf(JsonSyntaxException.class));
+    }
+
+    @Test
+    void testHandleLongPollResponseNPE() {
+        doThrow(NullPointerException.class).when(longPollHandler).accept(any());
+
+        var resultJson = """
+                {
+                    "result": [
+                        {
+                            "@type": "DeviceServiceData",
+                            "deleted": true,
+                            "id": "CommunicationQuality",
+                            "deviceId": "hdm:ZigBee:30fb10fffe46d732"
+                        }
+                    ],
+                    "jsonrpc":"2.0"
+                }
+                """;
+        fixture.handleLongPollResponse(httpClient, "subscriptionId", resultJson);
+
+        ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
+        verify(failureHandler).accept(throwableCaptor.capture());
+        Throwable t = throwableCaptor.getValue();
+        assertThat(t.getMessage(), is("Error while handling long poll response: '" + resultJson + "'"));
+        assertThat(t.getCause(), instanceOf(NullPointerException.class));
     }
 
     @AfterEach
index dbff9b7b0da839caad80c711154f04402dbb8eb8..b3b62cef5f0e8d9c8e954d44f1635d5a7a9deae9 100644 (file)
@@ -53,7 +53,7 @@ public class UserDefinedStateTest {
     @Test
     void testToString() {
         assertEquals(
-                String.format("UserDefinedState{id='%s', name='test user state', state=true, type='UserDefinedState'}",
+                String.format("UserDefinedState{id='%s', name='test user state', state=true, type='userDefinedState'}",
                         testId),
                 fixture.toString());
     }
index 252983adbe543f6e7808729e0462670b973e34fd..e77860dd0f4992884dccb1ea03612c8f7b29e681 100644 (file)
  */
 package org.openhab.binding.boschshc.internal.serialization;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.hasSize;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.util.HashSet;
+import java.util.List;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.junit.jupiter.api.Test;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Scenario;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedState;
+import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
 
 /**
  * Unit tests for {@link BoschServiceDataDeserializer}.
  *
  * @author Patrick Gell - Initial contribution
+ * @author David Pace - Added tests for all supported service data classes
  *
  */
 @NonNullByDefault
@@ -60,12 +67,104 @@ class BoschServiceDataDeserializerTest {
                 """;
 
         var longPollResult = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(resultJson, LongPollResult.class);
+        // note: when using assertThat() to check that the value is non-null, we get compiler warnings.
         assertNotNull(longPollResult);
-        assertEquals(2, longPollResult.result.size());
+        List<BoschSHCServiceState> results = longPollResult.result;
+        assertThat(results, is(notNullValue()));
+        assertThat(results, hasSize(2));
 
-        var resultClasses = new HashSet<>(longPollResult.result.stream().map(e -> e.getClass().getName()).toList());
-        assertEquals(2, resultClasses.size());
-        assertTrue(resultClasses.contains(DeviceServiceData.class.getName()));
-        assertTrue(resultClasses.contains(Scenario.class.getName()));
+        var resultClasses = new HashSet<>(results.stream().map(e -> e.getClass().getName()).toList());
+        assertThat(resultClasses, hasSize(2));
+        assertThat(resultClasses, containsInAnyOrder(DeviceServiceData.class.getName(), Scenario.class.getName()));
+    }
+
+    @Test
+    void testDeserializeDeletedDeviceServiceData() {
+        var resultJson = """
+                {
+                    "result": [
+                        {
+                            "@type": "DeviceServiceData",
+                            "deleted": true,
+                            "id": "CommunicationQuality",
+                            "deviceId": "hdm:ZigBee:30fb10fffe46d732"
+                        }
+                    ],
+                    "jsonrpc":"2.0"
+                }
+                """;
+
+        var longPollResult = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(resultJson, LongPollResult.class);
+        // note: when using assertThat() to check that the value is non-null, we get compiler warnings.
+        assertNotNull(longPollResult);
+        List<BoschSHCServiceState> results = longPollResult.result;
+        assertThat(results, is(notNullValue()));
+        assertThat(results, hasSize(1));
+
+        DeviceServiceData deviceServiceData = (DeviceServiceData) longPollResult.result.get(0);
+        assertThat(deviceServiceData.type, is("DeviceServiceData"));
+        assertThat(deviceServiceData.id, is("CommunicationQuality"));
+        assertThat(deviceServiceData.deviceId, is("hdm:ZigBee:30fb10fffe46d732"));
+    }
+
+    @Test
+    void testDeserializeScenarioTriggered() {
+        String resultJson = """
+                {
+                    "result": [
+                        {
+                            "@type": "scenarioTriggered",
+                            "name": "My Scenario",
+                            "id": "509bd737-eed0-40b7-8caa-e8686a714399",
+                            "lastTimeTriggered": "1693758693032"
+                        }
+                    ],
+                    "jsonrpc": "2.0"
+                }
+                """;
+
+        var longPollResult = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(resultJson, LongPollResult.class);
+        // note: when using assertThat() to check that the value is non-null, we get compiler warnings.
+        assertNotNull(longPollResult);
+        List<BoschSHCServiceState> results = longPollResult.result;
+        assertThat(results, is(notNullValue()));
+        assertThat(results, hasSize(1));
+
+        Scenario scenario = (Scenario) longPollResult.result.get(0);
+        assertThat(scenario.type, is("scenarioTriggered"));
+        assertThat(scenario.name, is("My Scenario"));
+        assertThat(scenario.id, is("509bd737-eed0-40b7-8caa-e8686a714399"));
+        assertThat(scenario.lastTimeTriggered, is("1693758693032"));
+    }
+
+    @Test
+    void testDeserializeUserDefinedState() {
+        String resultJson = """
+                {
+                    "result": [
+                        {
+                            "@type": "userDefinedState",
+                            "deleted": false,
+                            "name": "Test State",
+                            "id": "3d8023d6-69ca-4e79-89dd-7090295cefbf",
+                            "state": true
+                        }
+                    ],
+                    "jsonrpc": "2.0"
+                }
+                """;
+
+        var longPollResult = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(resultJson, LongPollResult.class);
+        // note: when using assertThat() to check that the value is non-null, we get compiler warnings.
+        assertNotNull(longPollResult);
+        List<BoschSHCServiceState> results = longPollResult.result;
+        assertThat(results, is(notNullValue()));
+        assertThat(results, hasSize(1));
+
+        UserDefinedState userDefinedState = (UserDefinedState) longPollResult.result.get(0);
+        assertThat(userDefinedState.type, is("userDefinedState"));
+        assertThat(userDefinedState.getName(), is("Test State"));
+        assertThat(userDefinedState.getId(), is("3d8023d6-69ca-4e79-89dd-7090295cefbf"));
+        assertThat(userDefinedState.isState(), is(true));
     }
 }
index 9a127e9fed1a3c0fca0d8e0796c43fba8408278d..a1e8295892ddddba50b2e48606045f5618f53421 100644 (file)
  */
 package org.openhab.binding.boschshc.internal.services.dto;
 
-import static org.junit.jupiter.api.Assertions.*;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.junit.jupiter.api.Test;
@@ -85,8 +90,9 @@ class BoschSHCServiceStateTest {
     @Test
     void fromJsonReturnsUserStateServiceStateForValidJson() {
         var state = BoschSHCServiceState.fromJson(new JsonPrimitive("false"), UserStateServiceState.class);
-        assertNotEquals(null, state);
-        assertTrue(state.getClass().isAssignableFrom(UserStateServiceState.class));
-        assertFalse(state.isState());
+        // note: when using assertThat() to check that the value is non-null, we get compiler warnings.
+        assertNotNull(state);
+        assertThat(state, instanceOf(UserStateServiceState.class));
+        assertThat(state.isState(), is(false));
     }
 }