]> git.basschouten.com Git - openhab-addons.git/blob
2a8760767cf658b2337f8e217b924b621ec774da
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.util.MieleCloudBindingIntegrationTestConstants.*;
19 import static org.openhab.binding.mielecloud.internal.util.ReflectionUtil.*;
20
21 import java.util.Collections;
22 import java.util.Objects;
23 import java.util.Optional;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.junit.jupiter.api.BeforeEach;
28 import org.junit.jupiter.api.Test;
29 import org.mockito.Mockito;
30 import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
31 import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.I18NKeys;
32 import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
33 import org.openhab.binding.mielecloud.internal.auth.OpenHabOAuthTokenRefresher;
34 import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
35 import org.openhab.binding.mielecloud.internal.util.OpenHabOsgiTest;
36 import org.openhab.binding.mielecloud.internal.webservice.ConnectionError;
37 import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
38 import org.openhab.binding.mielecloud.internal.webservice.MieleWebserviceFactory;
39 import org.openhab.binding.mielecloud.internal.webservice.language.CombiningLanguageProvider;
40 import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
41 import org.openhab.core.auth.client.oauth2.OAuthClientService;
42 import org.openhab.core.auth.client.oauth2.OAuthFactory;
43 import org.openhab.core.config.core.Configuration;
44 import org.openhab.core.thing.Bridge;
45 import org.openhab.core.thing.ThingStatus;
46 import org.openhab.core.thing.ThingStatusDetail;
47 import org.openhab.core.thing.binding.ThingHandlerFactory;
48 import org.openhab.core.thing.binding.builder.BridgeBuilder;
49
50 /**
51  * @author Björn Lange - Initial contribution
52  */
53 @NonNullByDefault
54 public class MieleBridgeHandlerTest extends OpenHabOsgiTest {
55     private static final String SERVICE_HANDLE = MieleCloudBindingIntegrationTestConstants.EMAIL;
56     private static final String CONFIG_PARAM_LOCALE = "locale";
57
58     @Nullable
59     private MieleWebservice webserviceMock;
60     @Nullable
61     private String webserviceAccessToken;
62     @Nullable
63     private OAuthFactory oauthFactoryMock;
64     @Nullable
65     private OAuthClientService oauthClientServiceMock;
66
67     @Nullable
68     private Bridge bridge;
69     @Nullable
70     private MieleBridgeHandler handler;
71
72     private MieleWebservice getWebserviceMock() {
73         assertNotNull(webserviceMock);
74         return Objects.requireNonNull(webserviceMock);
75     }
76
77     private OAuthFactory getOAuthFactoryMock() {
78         assertNotNull(oauthFactoryMock);
79         return Objects.requireNonNull(oauthFactoryMock);
80     }
81
82     private OAuthClientService getOAuthClientServiceMock() {
83         OAuthClientService oauthClientServiceMock = this.oauthClientServiceMock;
84         assertNotNull(oauthClientServiceMock);
85         return Objects.requireNonNull(oauthClientServiceMock);
86     }
87
88     private Bridge getBridge() {
89         assertNotNull(bridge);
90         return Objects.requireNonNull(bridge);
91     }
92
93     private MieleBridgeHandler getHandler() {
94         assertNotNull(handler);
95         return Objects.requireNonNull(handler);
96     }
97
98     @BeforeEach
99     public void setUp() throws Exception {
100         setUpWebservice();
101         setUpBridgeThingAndHandler();
102         setUpOAuthFactory();
103     }
104
105     private void setUpWebservice() throws NoSuchFieldException, IllegalAccessException {
106         webserviceMock = mock(MieleWebservice.class);
107         doAnswer(invocation -> {
108             if (invocation != null) {
109                 webserviceAccessToken = invocation.getArgument(0);
110             }
111             return null;
112         }).when(getWebserviceMock()).setAccessToken(anyString());
113         when(getWebserviceMock().hasAccessToken()).then(invocation -> webserviceAccessToken != null);
114
115         MieleWebserviceFactory webserviceFactory = mock(MieleWebserviceFactory.class);
116         when(webserviceFactory.create(any())).thenReturn(getWebserviceMock());
117
118         MieleHandlerFactory handlerFactory = getService(ThingHandlerFactory.class, MieleHandlerFactory.class);
119         assertNotNull(handlerFactory);
120         setPrivate(Objects.requireNonNull(handlerFactory), "webserviceFactory", webserviceFactory);
121     }
122
123     private void setUpBridgeThingAndHandler() {
124         when(getWebserviceMock().hasAccessToken()).thenReturn(false);
125
126         bridge = BridgeBuilder
127                 .create(MieleCloudBindingConstants.THING_TYPE_BRIDGE,
128                         MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID)
129                 .withConfiguration(
130                         new Configuration(Collections.singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL,
131                                 MieleCloudBindingIntegrationTestConstants.EMAIL)))
132                 .withLabel(MIELE_CLOUD_ACCOUNT_LABEL).build();
133         assertNotNull(bridge);
134
135         getThingRegistry().add(getBridge());
136
137         waitForAssert(() -> {
138             assertNotNull(getBridge().getHandler());
139             assertTrue(getBridge().getHandler() instanceof MieleBridgeHandler, "Handler type is wrong");
140         });
141         handler = (MieleBridgeHandler) getBridge().getHandler();
142     }
143
144     private void setUpOAuthFactory() throws Exception {
145         AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
146         accessTokenResponse.setAccessToken(ACCESS_TOKEN);
147
148         oauthClientServiceMock = mock(OAuthClientService.class);
149         when(oauthClientServiceMock.getAccessTokenResponse()).thenReturn(accessTokenResponse);
150
151         OAuthFactory oAuthFactory = mock(OAuthFactory.class);
152         Mockito.when(oAuthFactory.getOAuthClientService(SERVICE_HANDLE)).thenReturn(getOAuthClientServiceMock());
153         oauthFactoryMock = oAuthFactory;
154
155         OpenHabOAuthTokenRefresher tokenRefresher = getService(OAuthTokenRefresher.class,
156                 OpenHabOAuthTokenRefresher.class);
157         assertNotNull(tokenRefresher);
158         setPrivate(Objects.requireNonNull(tokenRefresher), "oauthFactory", oAuthFactory);
159     }
160
161     private void initializeBridgeWithTokens() {
162         getHandler().initialize();
163         assertThingStatusIs(ThingStatus.UNKNOWN);
164     }
165
166     private void assertThingStatusIs(ThingStatus expectedStatus) {
167         assertThingStatusIs(expectedStatus, ThingStatusDetail.NONE);
168     }
169
170     private void assertThingStatusIs(ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail) {
171         assertThingStatusIs(expectedStatus, expectedStatusDetail, null);
172     }
173
174     private void assertThingStatusIs(ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail,
175             @Nullable String expectedDescription) {
176         assertEquals(expectedStatus, getBridge().getStatus());
177         assertEquals(expectedStatusDetail, getBridge().getStatusInfo().getStatusDetail());
178         if (expectedDescription == null) {
179             assertNull(getBridge().getStatusInfo().getDescription());
180         } else {
181             assertEquals(expectedDescription, getBridge().getStatusInfo().getDescription());
182         }
183     }
184
185     @Test
186     public void testThingStatusIsSetToOfflineWithDetailConfigurationPendingAndDescriptionWhenTokensAreNotPassedViaInitialConfiguration()
187             throws Exception {
188         when(getOAuthClientServiceMock().getAccessTokenResponse()).thenReturn(null);
189
190         // when:
191         getHandler().initialize();
192
193         // then:
194         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
195                 MieleCloudBindingConstants.I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_NOT_CONFIGURED);
196     }
197
198     @Test
199     public void testThingStatusIsSetToOfflineWithDetailConfigurationErrorAndDescriptionWhenTheEmailAddressIsInvalid()
200             throws Exception {
201         // given:
202         getBridge().getConfiguration().setProperties(
203                 Collections.singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL, "not!a!mail$address"));
204
205         // when:
206         getHandler().initialize();
207
208         // then:
209         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
210                 MieleCloudBindingConstants.I18NKeys.BRIDGE_STATUS_DESCRIPTION_INVALID_EMAIL);
211     }
212
213     @Test
214     public void testThingStatusIsSetToOfflineWithDetailConfigurationErrorAndDescriptionWhenTheMieleAccountHasNotBeenAuthorized()
215             throws Exception {
216         // given:
217         OAuthFactory oAuthFactory = mock(OAuthFactory.class);
218         Mockito.when(oAuthFactory.getOAuthClientService(SERVICE_HANDLE)).thenReturn(null);
219
220         OpenHabOAuthTokenRefresher tokenRefresher = getService(OAuthTokenRefresher.class,
221                 OpenHabOAuthTokenRefresher.class);
222         assertNotNull(tokenRefresher);
223         // Clear the setup configuration and use the failing one for this test.
224         setPrivate(Objects.requireNonNull(tokenRefresher), "oauthFactory", oAuthFactory);
225
226         // when:
227         getHandler().initialize();
228
229         // then:
230         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
231                 MieleCloudBindingConstants.I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCOUNT_NOT_AUTHORIZED);
232     }
233
234     @Test
235     public void testThingStatusIsSetToUnknownAndThingWaitsForCloudConnectionWhenTheMieleAccountBecomesAuthorizedAfterTheBridgeWasInitialized()
236             throws Exception {
237         // given:
238         OAuthFactory oAuthFactory = mock(OAuthFactory.class);
239         Mockito.when(oAuthFactory.getOAuthClientService(SERVICE_HANDLE)).thenReturn(null);
240
241         OpenHabOAuthTokenRefresher tokenRefresher = getService(OAuthTokenRefresher.class,
242                 OpenHabOAuthTokenRefresher.class);
243         assertNotNull(tokenRefresher);
244         // Clear the setup configuration and use the failing one for this test.
245         setPrivate(Objects.requireNonNull(tokenRefresher), "oauthFactory", oAuthFactory);
246
247         getHandler().initialize();
248
249         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
250                 I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCOUNT_NOT_AUTHORIZED);
251
252         setUpOAuthFactory();
253
254         // when:
255         getHandler().dispose();
256         getHandler().initialize();
257
258         // then:
259         assertThingStatusIs(ThingStatus.UNKNOWN);
260     }
261
262     @Test
263     public void whenTheSseConnectionIsEstablishedThenTheThingStatusIsSetToOnline() throws Exception {
264         // given:
265         initializeBridgeWithTokens();
266
267         // when:
268         getHandler().onConnectionAlive();
269
270         // then:
271         assertThingStatusIs(ThingStatus.ONLINE);
272     }
273
274     @Test
275     public void whenAnAuthorizationFailedErrorIsReportedThenTheAccessTokenIsRefreshedAndTheSseConnectionRestored()
276             throws Exception {
277         // given:
278         AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
279         accessTokenResponse.setAccessToken(ACCESS_TOKEN);
280         when(getOAuthClientServiceMock().refreshToken()).thenReturn(accessTokenResponse);
281
282         initializeBridgeWithTokens();
283         getHandler().onConnectionAlive();
284
285         // when:
286         getHandler().onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
287
288         // then:
289         verify(getOAuthClientServiceMock()).refreshToken();
290         verify(getWebserviceMock()).connectSse();
291         assertThingStatusIs(ThingStatus.ONLINE);
292     }
293
294     @Test
295     public void whenAnAuthorizationFailedErrorIsReportedAndTokenRefreshFailsThenSseConnectionIsTerminatedAndTheStatusSetToOfflineWithDetailConfigurationError()
296             throws Exception {
297         // given:
298         when(getOAuthClientServiceMock().refreshToken()).thenReturn(new AccessTokenResponse());
299         initializeBridgeWithTokens();
300         getHandler().onConnectionAlive();
301
302         // when:
303         getHandler().onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
304
305         // then:
306         verify(getOAuthClientServiceMock()).refreshToken();
307         verify(getWebserviceMock()).disconnectSse();
308         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
309                 I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_REFRESH_FAILED);
310     }
311
312     @Test
313     public void whenARequestExecutionFailedErrorIsReportedAndNoRetriesHaveBeenMadeThenItHasNoEffectOnTheThingStatus()
314             throws Exception {
315         // given:
316         initializeBridgeWithTokens();
317         getHandler().onConnectionAlive();
318
319         // when:
320         getHandler().onConnectionError(ConnectionError.REQUEST_EXECUTION_FAILED, 0);
321
322         // then:
323         assertThingStatusIs(ThingStatus.ONLINE);
324     }
325
326     @Test
327     public void whenARequestExecutionFailedErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
328             throws Exception {
329         // given:
330         initializeBridgeWithTokens();
331         getHandler().onConnectionAlive();
332
333         // when:
334         getHandler().onConnectionError(ConnectionError.REQUEST_EXECUTION_FAILED, 10);
335
336         // then:
337         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
338     }
339
340     @Test
341     public void whenARequestExecutionFailedErrorIsReportedAndThingIsInStatusUnknownThenTheThingStatusIsOfflineWithDetailCommunicationError()
342             throws Exception {
343         // given:
344         initializeBridgeWithTokens();
345
346         // when:
347         getHandler().onConnectionError(ConnectionError.REQUEST_EXECUTION_FAILED, 0);
348
349         // then:
350         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
351     }
352
353     @Test
354     public void whenAServiceUnavailableErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
355             throws Exception {
356         // given:
357         initializeBridgeWithTokens();
358         getHandler().onConnectionAlive();
359
360         // when:
361         getHandler().onConnectionError(ConnectionError.SERVICE_UNAVAILABLE, 10);
362
363         // then:
364         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
365     }
366
367     @Test
368     public void whenAResponseMalformedErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
369             throws Exception {
370         // given:
371         initializeBridgeWithTokens();
372
373         // when:
374         getHandler().onConnectionError(ConnectionError.RESPONSE_MALFORMED, 10);
375
376         // then:
377         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
378     }
379
380     @Test
381     public void whenATimeoutErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
382             throws Exception {
383         // given:
384         initializeBridgeWithTokens();
385
386         // when:
387         getHandler().onConnectionError(ConnectionError.TIMEOUT, 10);
388
389         // then:
390         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
391     }
392
393     @Test
394     public void whenATooManyRequestsErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
395             throws Exception {
396         // given:
397         initializeBridgeWithTokens();
398
399         // when:
400         getHandler().onConnectionError(ConnectionError.TOO_MANY_RERQUESTS, 10);
401
402         // then:
403         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
404     }
405
406     @Test
407     public void whenAServerErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
408             throws Exception {
409         // given:
410         initializeBridgeWithTokens();
411
412         // when:
413         getHandler().onConnectionError(ConnectionError.SERVER_ERROR, 10);
414
415         // then:
416         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
417                 I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
418     }
419
420     @Test
421     public void whenARequestInterruptedErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
422             throws Exception {
423         // given:
424         initializeBridgeWithTokens();
425
426         // when:
427         getHandler().onConnectionError(ConnectionError.REQUEST_INTERRUPTED, 10);
428
429         // then:
430         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
431                 I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
432     }
433
434     @Test
435     public void whenSomeOtherHttpErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
436             throws Exception {
437         // given:
438         initializeBridgeWithTokens();
439
440         // when:
441         getHandler().onConnectionError(ConnectionError.OTHER_HTTP_ERROR, 10);
442
443         // then:
444         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
445                 I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
446     }
447
448     @Test
449     public void whenARequestIsInterruptedDuringInitializationThenTheThingStatusIsNotModified() throws Exception {
450         // given:
451         initializeBridgeWithTokens();
452
453         // when:
454         getHandler().onConnectionError(ConnectionError.REQUEST_INTERRUPTED, 0);
455
456         // then:
457         assertThingStatusIs(ThingStatus.UNKNOWN);
458     }
459
460     @Test
461     public void whenTheAccessTokenWasRefreshedThenTheWebserviceIsSetIntoAnOperationalState()
462             throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
463         // given:
464         getHandler().initialize();
465
466         // when:
467         getHandler().onNewAccessToken(ACCESS_TOKEN);
468
469         // then:
470         verify(getWebserviceMock(), atLeast(1)).setAccessToken(ACCESS_TOKEN);
471         verify(getWebserviceMock(), atLeast(1)).connectSse();
472     }
473
474     @Test
475     public void whenTheHandlerIsDisposedThenTheSseConnectionIsDisconnectedAndTheLanguageProviderIsUnset()
476             throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
477         // given:
478         getHandler().initialize();
479
480         // when:
481         getHandler().dispose();
482
483         // then:
484         verify(getWebserviceMock()).disconnectSse();
485
486         CombiningLanguageProvider languageProvider = getPrivate(getHandler(), "languageProvider");
487         assertNull(getPrivate(languageProvider, "prioritizedLanguageProvider"));
488     }
489
490     @Test
491     public void testNoLanguageIsReturnedWhenTheConfigurationParameterIsNotSet() {
492         // when:
493         Optional<String> language = getHandler().getLanguage();
494
495         // then:
496         assertFalse(language.isPresent());
497     }
498
499     @Test
500     public void testNoLanguageIsReturnedWhenTheConfigurationParameterIsEmpty() {
501         // given:
502         getHandler().handleConfigurationUpdate(Collections.singletonMap(CONFIG_PARAM_LOCALE, ""));
503
504         // when:
505         Optional<String> language = getHandler().getLanguage();
506
507         // then:
508         assertFalse(language.isPresent());
509     }
510
511     @Test
512     public void testNoLanguageIsReturnedWhenTheConfigurationParameterIsNotAValidTwoLetterLanguageCode() {
513         // given:
514         getHandler().handleConfigurationUpdate(Collections.singletonMap(CONFIG_PARAM_LOCALE, "Deutsch"));
515
516         // when:
517         Optional<String> language = getHandler().getLanguage();
518
519         // then:
520         assertFalse(language.isPresent());
521     }
522
523     @Test
524     public void testAValidTwoLetterLanguageCodeIsReturnedWhenTheConfigurationParameterIsSetToTheTwoLetterLanguageCode() {
525         // given:
526         getHandler().handleConfigurationUpdate(Collections.singletonMap(CONFIG_PARAM_LOCALE, "DE"));
527
528         // when:
529         String language = getHandler().getLanguage().get();
530
531         // then:
532         assertEquals("DE", language);
533     }
534
535     @Test
536     public void testWhenTheThingIsRemovedThenTheWebserviceIsLoggedOut() throws Exception {
537         // given:
538         initializeBridgeWithTokens();
539
540         // when:
541         getThingRegistry().remove(getHandler().getThing().getUID());
542
543         // then:
544         waitForAssert(() -> {
545             verify(getWebserviceMock()).logout();
546         });
547     }
548
549     @Test
550     public void testWhenTheThingIsRemovedThenTheTokensAreRemovedFromTheStorage() throws Exception {
551         // given:
552         initializeBridgeWithTokens();
553
554         // when:
555         getThingRegistry().remove(getHandler().getThing().getUID());
556
557         // then:
558         waitForAssert(() -> {
559             verify(getOAuthFactoryMock()).deleteServiceAndAccessToken(SERVICE_HANDLE);
560         });
561     }
562 }