]> git.basschouten.com Git - openhab-addons.git/blob
0ae26d7e33ba15dd6f30b0050e5727b1efd6b143
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.mielecloud.internal.handler;
14
15 import static org.junit.jupiter.api.Assertions.*;
16 import static org.mockito.ArgumentMatchers.*;
17 import static org.mockito.Mockito.*;
18 import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
19 import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.*;
20 import static org.openhab.binding.mielecloud.internal.util.ReflectionUtil.setPrivate;
21
22 import java.util.ArrayList;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Objects;
26 import java.util.Optional;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.junit.jupiter.api.AfterEach;
31 import org.junit.jupiter.api.BeforeEach;
32 import org.junit.jupiter.api.Test;
33 import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
34 import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
35 import org.openhab.binding.mielecloud.internal.auth.OpenHabOAuthTokenRefresher;
36 import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
37 import org.openhab.binding.mielecloud.internal.util.ReflectionUtil;
38 import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
39 import org.openhab.binding.mielecloud.internal.webservice.MieleWebserviceFactory;
40 import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
41 import org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus;
42 import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
43 import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
44 import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
45 import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
46 import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
47 import org.openhab.core.auth.client.oauth2.OAuthClientService;
48 import org.openhab.core.auth.client.oauth2.OAuthFactory;
49 import org.openhab.core.config.core.Configuration;
50 import org.openhab.core.items.Item;
51 import org.openhab.core.items.ItemBuilder;
52 import org.openhab.core.items.ItemBuilderFactory;
53 import org.openhab.core.items.ItemRegistry;
54 import org.openhab.core.library.types.DecimalType;
55 import org.openhab.core.library.types.OnOffType;
56 import org.openhab.core.library.types.StringType;
57 import org.openhab.core.test.java.JavaOSGiTest;
58 import org.openhab.core.thing.Bridge;
59 import org.openhab.core.thing.Channel;
60 import org.openhab.core.thing.ChannelUID;
61 import org.openhab.core.thing.Thing;
62 import org.openhab.core.thing.ThingRegistry;
63 import org.openhab.core.thing.ThingStatus;
64 import org.openhab.core.thing.ThingStatusDetail;
65 import org.openhab.core.thing.ThingTypeUID;
66 import org.openhab.core.thing.ThingUID;
67 import org.openhab.core.thing.binding.ThingHandler;
68 import org.openhab.core.thing.binding.ThingHandlerFactory;
69 import org.openhab.core.thing.binding.builder.BridgeBuilder;
70 import org.openhab.core.thing.binding.builder.ChannelBuilder;
71 import org.openhab.core.thing.binding.builder.ThingBuilder;
72 import org.openhab.core.thing.link.ItemChannelLink;
73 import org.openhab.core.thing.link.ItemChannelLinkRegistry;
74 import org.openhab.core.thing.type.ChannelDefinition;
75 import org.openhab.core.thing.type.ChannelType;
76 import org.openhab.core.thing.type.ChannelTypeRegistry;
77 import org.openhab.core.thing.type.ChannelTypeUID;
78 import org.openhab.core.thing.type.ThingType;
79 import org.openhab.core.thing.type.ThingTypeRegistry;
80 import org.openhab.core.types.State;
81 import org.openhab.core.types.UnDefType;
82
83 /**
84  * @author Björn Lange - Initial contribution
85  */
86 @NonNullByDefault
87 public abstract class AbstractMieleThingHandlerTest extends JavaOSGiTest {
88     protected static final State NULL_VALUE_STATE = UnDefType.UNDEF;
89
90     @Nullable
91     private Bridge bridge;
92     @Nullable
93     private MieleBridgeHandler bridgeHandler;
94     @Nullable
95     private ThingRegistry thingRegistry;
96     @Nullable
97     private MieleWebservice webserviceMock;
98     @Nullable
99     private AbstractMieleThingHandler thingHandler;
100
101     @Nullable
102     private ItemRegistry itemRegistry;
103
104     protected Bridge getBridge() {
105         assertNotNull(bridge);
106         return Objects.requireNonNull(bridge);
107     }
108
109     protected MieleBridgeHandler getBridgeHandler() {
110         assertNotNull(bridgeHandler);
111         return Objects.requireNonNull(bridgeHandler);
112     }
113
114     protected ThingRegistry getThingRegistry() {
115         assertNotNull(thingRegistry);
116         return Objects.requireNonNull(thingRegistry);
117     }
118
119     protected MieleWebservice getWebserviceMock() {
120         assertNotNull(webserviceMock);
121         return Objects.requireNonNull(webserviceMock);
122     }
123
124     protected AbstractMieleThingHandler getThingHandler() {
125         assertNotNull(thingHandler);
126         return Objects.requireNonNull(thingHandler);
127     }
128
129     protected ItemRegistry getItemRegistry() {
130         assertNotNull(itemRegistry);
131         return Objects.requireNonNull(itemRegistry);
132     }
133
134     @Override
135     protected void waitForAssert(Runnable assertion) {
136         // Use a larger timeout to avoid test failures in the CI build.
137         waitForAssert(assertion, 2 * DFL_TIMEOUT, 2 * DFL_SLEEP_TIME);
138     }
139
140     private void setUpThingRegistry() {
141         thingRegistry = getService(ThingRegistry.class, ThingRegistry.class);
142         assertNotNull(thingRegistry, "Thing registry is missing");
143     }
144
145     private void setUpItemRegistry() {
146         itemRegistry = getService(ItemRegistry.class, ItemRegistry.class);
147         assertNotNull(itemRegistry);
148     }
149
150     private void setUpWebservice()
151             throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
152         webserviceMock = mock(MieleWebservice.class);
153         when(getWebserviceMock().hasAccessToken()).thenReturn(true);
154
155         MieleWebserviceFactory webserviceFactory = mock(MieleWebserviceFactory.class);
156         when(webserviceFactory.create(any())).thenReturn(getWebserviceMock());
157
158         MieleHandlerFactory factory = getService(ThingHandlerFactory.class, MieleHandlerFactory.class);
159         assertNotNull(factory);
160         setPrivate(Objects.requireNonNull(factory), "webserviceFactory", webserviceFactory);
161     }
162
163     private void setUpBridge() throws Exception {
164         AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
165         accessTokenResponse.setAccessToken(ACCESS_TOKEN);
166
167         OAuthClientService oAuthClientService = mock(OAuthClientService.class);
168         when(oAuthClientService.getAccessTokenResponse()).thenReturn(accessTokenResponse);
169
170         OAuthFactory oAuthFactory = mock(OAuthFactory.class);
171         when(oAuthFactory
172                 .getOAuthClientService(MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString()))
173                 .thenReturn(oAuthClientService);
174
175         OpenHabOAuthTokenRefresher tokenRefresher = getService(OAuthTokenRefresher.class,
176                 OpenHabOAuthTokenRefresher.class);
177         assertNotNull(tokenRefresher);
178         setPrivate(Objects.requireNonNull(tokenRefresher), "oauthFactory", oAuthFactory);
179
180         bridge = BridgeBuilder
181                 .create(MieleCloudBindingConstants.THING_TYPE_BRIDGE,
182                         MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID)
183                 .withLabel("Miele@home Account")
184                 .withConfiguration(new Configuration(Map.of(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL,
185                         MieleCloudBindingIntegrationTestConstants.EMAIL)))
186                 .build();
187         assertNotNull(bridge);
188
189         getThingRegistry().add(getBridge());
190
191         // then:
192         waitForAssert(() -> {
193             assertNotNull(getBridge().getHandler());
194             assertTrue(getBridge().getHandler() instanceof MieleBridgeHandler, "Handler type is wrong");
195         });
196
197         MieleBridgeHandler bridgeHandler = (MieleBridgeHandler) getBridge().getHandler();
198         assertNotNull(bridgeHandler);
199
200         waitForAssert(() -> {
201             assertNotNull(bridgeHandler.getThing());
202         });
203
204         bridgeHandler.initialize();
205         bridgeHandler.onConnectionAlive();
206         setPrivate(bridgeHandler, "discoveryService", null);
207         this.bridgeHandler = bridgeHandler;
208     }
209
210     protected AbstractMieleThingHandler createThingHandler(ThingTypeUID thingTypeUid, ThingUID thingUid,
211             Class<? extends AbstractMieleThingHandler> expectedHandlerClass, String deviceIdentifier,
212             String thingTypeVersion) {
213         ThingRegistry registry = getThingRegistry();
214
215         List<Channel> channels = createChannelsForThingHandler(thingTypeUid, thingUid);
216
217         Thing thing = ThingBuilder.create(thingTypeUid, thingUid)
218                 .withConfiguration(new Configuration(
219                         Map.of(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER, deviceIdentifier)))
220                 .withBridge(getBridge().getUID()).withChannels(channels).withLabel("DA-6996")
221                 .withProperty("thingTypeVersion", thingTypeVersion).build();
222         assertNotNull(thing);
223
224         registry.add(thing);
225
226         waitForAssert(() -> {
227             ThingHandler handler = thing.getHandler();
228             assertNotNull(handler);
229             assertTrue(expectedHandlerClass.isAssignableFrom(handler.getClass()), "Handler type is wrong");
230         });
231
232         createItemsForChannels(thing);
233         linkChannelsToItems(thing);
234
235         ThingHandler handler = thing.getHandler();
236         assertNotNull(handler);
237         AbstractMieleThingHandler mieleThingHandler = (AbstractMieleThingHandler) Objects.requireNonNull(handler);
238
239         waitForAssert(() -> {
240             try {
241                 assertNotNull(ReflectionUtil.invokePrivate(mieleThingHandler, "getBridge"));
242             } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException e) {
243                 throw new RuntimeException(e);
244             }
245             assertNotNull(getBridge().getThing(thingUid));
246         });
247
248         return mieleThingHandler;
249     }
250
251     private List<Channel> createChannelsForThingHandler(ThingTypeUID thingTypeUid, ThingUID thingUid) {
252         ChannelTypeRegistry channelTypeRegistry = getService(ChannelTypeRegistry.class, ChannelTypeRegistry.class);
253         assertNotNull(channelTypeRegistry);
254
255         ThingTypeRegistry thingTypeRegistry = getService(ThingTypeRegistry.class, ThingTypeRegistry.class);
256         assertNotNull(thingTypeRegistry);
257
258         ThingType thingType = thingTypeRegistry.getThingType(thingTypeUid);
259         assertNotNull(thingType);
260
261         List<ChannelDefinition> channelDefinitions = thingType.getChannelDefinitions();
262         assertNotNull(channelDefinitions);
263
264         List<Channel> channels = new ArrayList<Channel>();
265         for (ChannelDefinition channelDefinition : channelDefinitions) {
266             ChannelTypeUID channelTypeUid = channelDefinition.getChannelTypeUID();
267             assertNotNull(channelTypeUid);
268
269             ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUid);
270             assertNotNull(channelType);
271
272             String acceptedItemType = channelType.getItemType();
273             assertNotNull(acceptedItemType);
274
275             String channelId = channelDefinition.getId();
276             assertNotNull(channelId);
277
278             ChannelUID channelUid = new ChannelUID(thingUid, channelId);
279             assertNotNull(channelUid);
280
281             Channel channel = ChannelBuilder.create(channelUid, acceptedItemType).build();
282             assertNotNull(channel);
283
284             channels.add(channel);
285         }
286
287         return channels;
288     }
289
290     private void createItemsForChannels(Thing thing) {
291         ItemBuilderFactory itemBuilderFactory = getService(ItemBuilderFactory.class);
292         assertNotNull(itemBuilderFactory);
293
294         for (Channel channel : thing.getChannels()) {
295             String acceptedItemType = channel.getAcceptedItemType();
296             assertNotNull(acceptedItemType);
297
298             ItemBuilder itemBuilder = itemBuilderFactory.newItemBuilder(Objects.requireNonNull(acceptedItemType),
299                     channel.getUID().getId());
300             assertNotNull(itemBuilder);
301
302             Item item = itemBuilder.build();
303             assertNotNull(item);
304
305             getItemRegistry().add(item);
306         }
307     }
308
309     private void linkChannelsToItems(Thing thing) {
310         ItemChannelLinkRegistry itemChannelLinkRegistry = getService(ItemChannelLinkRegistry.class,
311                 ItemChannelLinkRegistry.class);
312         assertNotNull(itemChannelLinkRegistry);
313
314         for (Channel channel : thing.getChannels()) {
315             String itemName = channel.getUID().getId();
316             assertNotNull(itemName);
317
318             ChannelUID channelUid = channel.getUID();
319             assertNotNull(channelUid);
320
321             ItemChannelLink link = itemChannelLinkRegistry.add(new ItemChannelLink(itemName, channelUid));
322             assertNotNull(link);
323         }
324     }
325
326     protected ChannelUID channel(String id) {
327         return new ChannelUID(getThingHandler().getThing().getUID(), id);
328     }
329
330     @BeforeEach
331     public void setUpAbstractMieleThingHandlerTest() throws Exception {
332         registerVolatileStorageService();
333         setUpThingRegistry();
334         setUpItemRegistry();
335         setUpWebservice();
336     }
337
338     protected void setUpBridgeAndThing() throws Exception {
339         setUpBridge();
340         thingHandler = setUpThingHandler();
341     }
342
343     private void assertThingStatusIs(Thing thing, ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail) {
344         assertThingStatusIs(thing, expectedStatus, expectedStatusDetail, null);
345     }
346
347     private void assertThingStatusIs(Thing thing, ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail,
348             @Nullable String expectedDescription) {
349         assertEquals(expectedStatus, thing.getStatus());
350         assertEquals(expectedStatusDetail, thing.getStatusInfo().getStatusDetail());
351         if (expectedDescription == null) {
352             assertNull(thing.getStatusInfo().getDescription());
353         } else {
354             assertEquals(expectedDescription, thing.getStatusInfo().getDescription());
355         }
356     }
357
358     protected State getChannelState(String channelUid) {
359         Item item = getItemRegistry().get(channelUid);
360         assertNotNull(item, "Item for channel UID " + channelUid + " is null.");
361         return item.getState();
362     }
363
364     /**
365      * Sets up the {@link ThingHandler} under test.
366      *
367      * @return The created {@link ThingHandler}.
368      */
369     protected abstract AbstractMieleThingHandler setUpThingHandler();
370
371     @AfterEach
372     public void tearDownAbstractMieleThingHandlerTest() {
373         getThingRegistry().forceRemove(getThingHandler().getThing().getUID());
374         getThingRegistry().forceRemove(getBridge().getUID());
375     }
376
377     @Test
378     public void testCachedStateIsQueriedOnInitialize() throws Exception {
379         // given:
380         setUpBridgeAndThing();
381
382         // then:
383         verify(getWebserviceMock()).dispatchDeviceState(SERIAL_NUMBER);
384     }
385
386     @Test
387     public void testThingStatusIsOfflineWithDetailGoneAndDetailMessageWhenDeviceIsRemoved() throws Exception {
388         // given:
389         setUpBridgeAndThing();
390
391         // when:
392         getBridgeHandler().onDeviceRemoved(SERIAL_NUMBER);
393
394         // then:
395         Thing thing = getThingHandler().getThing();
396         assertThingStatusIs(thing, ThingStatus.OFFLINE, ThingStatusDetail.GONE,
397                 "@text/mielecloud.thing.status.removed");
398     }
399
400     private DeviceState createDeviceStateMock(StateType stateType, String localizedState) {
401         DeviceState deviceState = mock(DeviceState.class);
402         when(deviceState.getDeviceIdentifier()).thenReturn(getThingHandler().getThing().getUID().getId());
403         when(deviceState.getRawType()).thenReturn(DeviceType.UNKNOWN);
404         when(deviceState.getStateType()).thenReturn(Optional.of(stateType));
405         when(deviceState.isInState(any())).thenCallRealMethod();
406         when(deviceState.getStatus()).thenReturn(Optional.of(localizedState));
407         return deviceState;
408     }
409
410     @Test
411     public void testStatusIsSetToOnlineWhenDeviceStateIsValid() throws Exception {
412         // given:
413         setUpBridgeAndThing();
414
415         DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
416
417         // when:
418         getBridgeHandler().onDeviceStateUpdated(deviceState);
419
420         // then:
421         assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
422     }
423
424     @Test
425     public void testStatusIsSetToOfflineWhenDeviceIsNotConnected() throws Exception {
426         // given:
427         setUpBridgeAndThing();
428
429         DeviceState deviceState = createDeviceStateMock(StateType.NOT_CONNECTED, "Not connected");
430
431         // when:
432         getBridgeHandler().onDeviceStateUpdated(deviceState);
433
434         // then:
435         assertThingStatusIs(getThingHandler().getThing(), ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
436                 "@text/mielecloud.thing.status.disconnected");
437     }
438
439     @Test
440     public void testFailingPutProcessActionDoesNotSetTheDeviceToOffline() throws Exception {
441         // given:
442         doThrow(MieleWebserviceException.class).when(getWebserviceMock()).putProcessAction(any(),
443                 eq(ProcessAction.STOP));
444
445         setUpBridgeAndThing();
446
447         DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
448         getBridgeHandler().onDeviceStateUpdated(deviceState);
449         assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
450
451         // when:
452         getThingHandler().triggerProcessAction(ProcessAction.STOP);
453
454         // then:
455         assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
456     }
457
458     @Test
459     public void testHandleCommandProgramStartToStartStopChannel() throws Exception {
460         // given:
461         setUpBridgeAndThing();
462
463         // when:
464         getThingHandler().handleCommand(channel(PROGRAM_START_STOP),
465                 new StringType(ProgramStatus.PROGRAM_STARTED.getState()));
466
467         // then:
468         waitForAssert(() -> {
469             verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.START);
470         });
471     }
472
473     @Test
474     public void testHandleCommandProgramStopToStartStopChannel() throws Exception {
475         // given:
476         setUpBridgeAndThing();
477
478         // when:
479         getThingHandler().handleCommand(channel(PROGRAM_START_STOP),
480                 new StringType(ProgramStatus.PROGRAM_STOPPED.getState()));
481
482         // then:
483         waitForAssert(() -> {
484             verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.STOP);
485         });
486     }
487
488     @Test
489     public void testHandleCommandProgramStartToStartStopPauseChannel() throws Exception {
490         // given:
491         setUpBridgeAndThing();
492
493         // when:
494         getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
495                 new StringType(ProgramStatus.PROGRAM_STARTED.getState()));
496
497         // then:
498         waitForAssert(() -> {
499             verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.START);
500         });
501     }
502
503     @Test
504     public void testHandleCommandProgramStopToStartStopPauseChannel() throws Exception {
505         // given:
506         setUpBridgeAndThing();
507
508         // when:
509         getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
510                 new StringType(ProgramStatus.PROGRAM_STOPPED.getState()));
511
512         // then:
513         waitForAssert(() -> {
514             verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.STOP);
515         });
516     }
517
518     @Test
519     public void testHandleCommandProgramPauseToStartStopPauseChannel() throws Exception {
520         // given:
521         setUpBridgeAndThing();
522
523         // when:
524         getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
525                 new StringType(ProgramStatus.PROGRAM_PAUSED.getState()));
526
527         // then:
528         waitForAssert(() -> {
529             verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.PAUSE);
530         });
531     }
532
533     @Test
534     public void testFailingPutLightDoesNotSetTheDeviceToOffline() throws Exception {
535         // given:
536         doThrow(MieleWebserviceException.class).when(getWebserviceMock()).putLight(any(), eq(true));
537
538         setUpBridgeAndThing();
539
540         DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
541         getBridgeHandler().onDeviceStateUpdated(deviceState);
542         assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
543
544         // when:
545         getThingHandler().triggerLight(true);
546
547         // then:
548         assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
549     }
550
551     @Test
552     public void testHandleCommandLightOff() throws Exception {
553         // given:
554         setUpBridgeAndThing();
555
556         // when:
557         getThingHandler().handleCommand(channel(LIGHT_SWITCH), OnOffType.OFF);
558
559         // then:
560         waitForAssert(() -> {
561             verify(getWebserviceMock()).putLight(getThingHandler().getDeviceId(), false);
562         });
563     }
564
565     @Test
566     public void testHandleCommandLightOn() throws Exception {
567         // given:
568         setUpBridgeAndThing();
569
570         // when:
571         getThingHandler().handleCommand(channel(LIGHT_SWITCH), OnOffType.ON);
572
573         // then:
574         waitForAssert(() -> {
575             verify(getWebserviceMock()).putLight(getThingHandler().getDeviceId(), true);
576         });
577     }
578
579     @Test
580     public void testHandleCommandDoesNothingWhenCommandIsNotOfOnOffType() throws Exception {
581         // given:
582         setUpBridgeAndThing();
583
584         // when:
585         getThingHandler().handleCommand(channel(LIGHT_SWITCH), new DecimalType(0));
586
587         // then:
588         verify(getWebserviceMock(), never()).putLight(anyString(), anyBoolean());
589     }
590
591     @Test
592     public void testHandleCommandPowerOn() throws Exception {
593         // given:
594         setUpBridgeAndThing();
595
596         // when:
597         getThingHandler().handleCommand(channel(POWER_ON_OFF), OnOffType.ON);
598
599         // then:
600         waitForAssert(() -> {
601             verify(getWebserviceMock()).putPowerState(getThingHandler().getDeviceId(), true);
602         });
603     }
604
605     @Test
606     public void testHandleCommandPowerOff() throws Exception {
607         // given:
608         setUpBridgeAndThing();
609
610         // when:
611         getThingHandler().handleCommand(channel(POWER_ON_OFF), OnOffType.OFF);
612
613         // then:
614         waitForAssert(() -> {
615             verify(getWebserviceMock()).putPowerState(getThingHandler().getDeviceId(), false);
616         });
617     }
618
619     @Test
620     public void testHandleCommandDoesNothingWhenPowerCommandIsNotOfOnOffType() throws Exception {
621         // given:
622         setUpBridgeAndThing();
623
624         // when:
625         getThingHandler().handleCommand(channel(POWER_ON_OFF), new DecimalType(0));
626
627         // then:
628         verify(getWebserviceMock(), never()).putPowerState(anyString(), anyBoolean());
629     }
630
631     @Test
632     public void testMissingPropertiesAreSetWhenAStateUpdateIsReceivedFromTheCloud() throws Exception {
633         // given:
634         setUpBridgeAndThing();
635
636         assertFalse(getThingHandler().getThing().getProperties().containsKey(Thing.PROPERTY_SERIAL_NUMBER));
637         assertFalse(getThingHandler().getThing().getProperties().containsKey(Thing.PROPERTY_MODEL_ID));
638
639         var deviceState = mock(DeviceState.class);
640         when(deviceState.getRawType()).thenReturn(DeviceType.UNKNOWN);
641         when(deviceState.getDeviceIdentifier()).thenReturn(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
642         when(deviceState.getFabNumber())
643                 .thenReturn(Optional.of(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER));
644         when(deviceState.getType()).thenReturn(Optional.of("Unknown device type"));
645         when(deviceState.getTechType()).thenReturn(Optional.of("UK-4567"));
646
647         // when:
648         getThingHandler().onDeviceStateUpdated(deviceState);
649
650         // then:
651         assertEquals(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER,
652                 getThingHandler().getThing().getProperties().get(Thing.PROPERTY_SERIAL_NUMBER));
653         assertEquals("Unknown device type UK-4567",
654                 getThingHandler().getThing().getProperties().get(Thing.PROPERTY_MODEL_ID));
655     }
656 }