]> git.basschouten.com Git - openhab-addons.git/blob
8592c14cb4432d25beb2d110fc400a26f1661a5e
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.Map;
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(new Configuration(Map.of(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL,
130                         MieleCloudBindingIntegrationTestConstants.EMAIL)))
131                 .withLabel(MIELE_CLOUD_ACCOUNT_LABEL).build();
132         assertNotNull(bridge);
133
134         getThingRegistry().add(getBridge());
135
136         waitForAssert(() -> {
137             assertNotNull(getBridge().getHandler());
138             assertTrue(getBridge().getHandler() instanceof MieleBridgeHandler, "Handler type is wrong");
139         });
140         handler = (MieleBridgeHandler) getBridge().getHandler();
141     }
142
143     private void setUpOAuthFactory() throws Exception {
144         AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
145         accessTokenResponse.setAccessToken(ACCESS_TOKEN);
146
147         oauthClientServiceMock = mock(OAuthClientService.class);
148         when(oauthClientServiceMock.getAccessTokenResponse()).thenReturn(accessTokenResponse);
149
150         OAuthFactory oAuthFactory = mock(OAuthFactory.class);
151         Mockito.when(oAuthFactory.getOAuthClientService(SERVICE_HANDLE)).thenReturn(getOAuthClientServiceMock());
152         oauthFactoryMock = oAuthFactory;
153
154         OpenHabOAuthTokenRefresher tokenRefresher = getService(OAuthTokenRefresher.class,
155                 OpenHabOAuthTokenRefresher.class);
156         assertNotNull(tokenRefresher);
157         setPrivate(Objects.requireNonNull(tokenRefresher), "oauthFactory", oAuthFactory);
158     }
159
160     private void initializeBridgeWithTokens() {
161         getHandler().initialize();
162         assertThingStatusIs(ThingStatus.UNKNOWN);
163     }
164
165     private void assertThingStatusIs(ThingStatus expectedStatus) {
166         assertThingStatusIs(expectedStatus, ThingStatusDetail.NONE);
167     }
168
169     private void assertThingStatusIs(ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail) {
170         assertThingStatusIs(expectedStatus, expectedStatusDetail, null);
171     }
172
173     private void assertThingStatusIs(ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail,
174             @Nullable String expectedDescription) {
175         assertEquals(expectedStatus, getBridge().getStatus());
176         assertEquals(expectedStatusDetail, getBridge().getStatusInfo().getStatusDetail());
177         if (expectedDescription == null) {
178             assertNull(getBridge().getStatusInfo().getDescription());
179         } else {
180             assertEquals(expectedDescription, getBridge().getStatusInfo().getDescription());
181         }
182     }
183
184     @Test
185     public void testThingStatusIsSetToOfflineWithDetailConfigurationPendingAndDescriptionWhenTokensAreNotPassedViaInitialConfiguration()
186             throws Exception {
187         when(getOAuthClientServiceMock().getAccessTokenResponse()).thenReturn(null);
188
189         // when:
190         getHandler().initialize();
191
192         // then:
193         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
194                 MieleCloudBindingConstants.I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_NOT_CONFIGURED);
195     }
196
197     @Test
198     public void testThingStatusIsSetToOfflineWithDetailConfigurationErrorAndDescriptionWhenTheMieleAccountHasNotBeenAuthorized()
199             throws Exception {
200         // given:
201         OAuthFactory oAuthFactory = mock(OAuthFactory.class);
202         Mockito.when(oAuthFactory.getOAuthClientService(SERVICE_HANDLE)).thenReturn(null);
203
204         OpenHabOAuthTokenRefresher tokenRefresher = getService(OAuthTokenRefresher.class,
205                 OpenHabOAuthTokenRefresher.class);
206         assertNotNull(tokenRefresher);
207         // Clear the setup configuration and use the failing one for this test.
208         setPrivate(Objects.requireNonNull(tokenRefresher), "oauthFactory", oAuthFactory);
209
210         // when:
211         getHandler().initialize();
212
213         // then:
214         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
215                 MieleCloudBindingConstants.I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCOUNT_NOT_AUTHORIZED);
216     }
217
218     @Test
219     public void testThingStatusIsSetToUnknownAndThingWaitsForCloudConnectionWhenTheMieleAccountBecomesAuthorizedAfterTheBridgeWasInitialized()
220             throws Exception {
221         // given:
222         OAuthFactory oAuthFactory = mock(OAuthFactory.class);
223         Mockito.when(oAuthFactory.getOAuthClientService(SERVICE_HANDLE)).thenReturn(null);
224
225         OpenHabOAuthTokenRefresher tokenRefresher = getService(OAuthTokenRefresher.class,
226                 OpenHabOAuthTokenRefresher.class);
227         assertNotNull(tokenRefresher);
228         // Clear the setup configuration and use the failing one for this test.
229         setPrivate(Objects.requireNonNull(tokenRefresher), "oauthFactory", oAuthFactory);
230
231         getHandler().initialize();
232
233         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
234                 I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCOUNT_NOT_AUTHORIZED);
235
236         setUpOAuthFactory();
237
238         // when:
239         getHandler().dispose();
240         getHandler().initialize();
241
242         // then:
243         assertThingStatusIs(ThingStatus.UNKNOWN);
244     }
245
246     @Test
247     public void whenTheSseConnectionIsEstablishedThenTheThingStatusIsSetToOnline() throws Exception {
248         // given:
249         initializeBridgeWithTokens();
250
251         // when:
252         getHandler().onConnectionAlive();
253
254         // then:
255         assertThingStatusIs(ThingStatus.ONLINE);
256     }
257
258     @Test
259     public void whenAnAuthorizationFailedErrorIsReportedThenTheAccessTokenIsRefreshedAndTheSseConnectionRestored()
260             throws Exception {
261         // given:
262         AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
263         accessTokenResponse.setAccessToken(ACCESS_TOKEN);
264         when(getOAuthClientServiceMock().refreshToken()).thenReturn(accessTokenResponse);
265
266         initializeBridgeWithTokens();
267         getHandler().onConnectionAlive();
268
269         // when:
270         getHandler().onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
271
272         // then:
273         verify(getOAuthClientServiceMock()).refreshToken();
274         verify(getWebserviceMock()).connectSse();
275         assertThingStatusIs(ThingStatus.ONLINE);
276     }
277
278     @Test
279     public void whenAnAuthorizationFailedErrorIsReportedAndTokenRefreshFailsThenSseConnectionIsTerminatedAndTheStatusSetToOfflineWithDetailConfigurationError()
280             throws Exception {
281         // given:
282         when(getOAuthClientServiceMock().refreshToken()).thenReturn(new AccessTokenResponse());
283         initializeBridgeWithTokens();
284         getHandler().onConnectionAlive();
285
286         // when:
287         getHandler().onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
288
289         // then:
290         verify(getOAuthClientServiceMock()).refreshToken();
291         verify(getWebserviceMock()).disconnectSse();
292         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
293                 I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_REFRESH_FAILED);
294     }
295
296     @Test
297     public void whenARequestExecutionFailedErrorIsReportedAndNoRetriesHaveBeenMadeThenItHasNoEffectOnTheThingStatus()
298             throws Exception {
299         // given:
300         initializeBridgeWithTokens();
301         getHandler().onConnectionAlive();
302
303         // when:
304         getHandler().onConnectionError(ConnectionError.REQUEST_EXECUTION_FAILED, 0);
305
306         // then:
307         assertThingStatusIs(ThingStatus.ONLINE);
308     }
309
310     @Test
311     public void whenARequestExecutionFailedErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
312             throws Exception {
313         // given:
314         initializeBridgeWithTokens();
315         getHandler().onConnectionAlive();
316
317         // when:
318         getHandler().onConnectionError(ConnectionError.REQUEST_EXECUTION_FAILED, 10);
319
320         // then:
321         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
322     }
323
324     @Test
325     public void whenARequestExecutionFailedErrorIsReportedAndThingIsInStatusUnknownThenTheThingStatusIsOfflineWithDetailCommunicationError()
326             throws Exception {
327         // given:
328         initializeBridgeWithTokens();
329
330         // when:
331         getHandler().onConnectionError(ConnectionError.REQUEST_EXECUTION_FAILED, 0);
332
333         // then:
334         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
335     }
336
337     @Test
338     public void whenAServiceUnavailableErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
339             throws Exception {
340         // given:
341         initializeBridgeWithTokens();
342         getHandler().onConnectionAlive();
343
344         // when:
345         getHandler().onConnectionError(ConnectionError.SERVICE_UNAVAILABLE, 10);
346
347         // then:
348         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
349     }
350
351     @Test
352     public void whenAResponseMalformedErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
353             throws Exception {
354         // given:
355         initializeBridgeWithTokens();
356
357         // when:
358         getHandler().onConnectionError(ConnectionError.RESPONSE_MALFORMED, 10);
359
360         // then:
361         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
362     }
363
364     @Test
365     public void whenATimeoutErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
366             throws Exception {
367         // given:
368         initializeBridgeWithTokens();
369
370         // when:
371         getHandler().onConnectionError(ConnectionError.TIMEOUT, 10);
372
373         // then:
374         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
375     }
376
377     @Test
378     public void whenATooManyRequestsErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
379             throws Exception {
380         // given:
381         initializeBridgeWithTokens();
382
383         // when:
384         getHandler().onConnectionError(ConnectionError.TOO_MANY_RERQUESTS, 10);
385
386         // then:
387         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
388     }
389
390     @Test
391     public void whenAServerErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
392             throws Exception {
393         // given:
394         initializeBridgeWithTokens();
395
396         // when:
397         getHandler().onConnectionError(ConnectionError.SERVER_ERROR, 10);
398
399         // then:
400         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
401                 I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
402     }
403
404     @Test
405     public void whenARequestInterruptedErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
406             throws Exception {
407         // given:
408         initializeBridgeWithTokens();
409
410         // when:
411         getHandler().onConnectionError(ConnectionError.REQUEST_INTERRUPTED, 10);
412
413         // then:
414         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
415                 I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
416     }
417
418     @Test
419     public void whenSomeOtherHttpErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
420             throws Exception {
421         // given:
422         initializeBridgeWithTokens();
423
424         // when:
425         getHandler().onConnectionError(ConnectionError.OTHER_HTTP_ERROR, 10);
426
427         // then:
428         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
429                 I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
430     }
431
432     @Test
433     public void whenARequestIsInterruptedDuringInitializationThenTheThingStatusIsNotModified() throws Exception {
434         // given:
435         initializeBridgeWithTokens();
436
437         // when:
438         getHandler().onConnectionError(ConnectionError.REQUEST_INTERRUPTED, 0);
439
440         // then:
441         assertThingStatusIs(ThingStatus.UNKNOWN);
442     }
443
444     @Test
445     public void whenTheAccessTokenWasRefreshedThenTheWebserviceIsSetIntoAnOperationalState()
446             throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
447         // given:
448         getHandler().initialize();
449
450         // when:
451         getHandler().onNewAccessToken(ACCESS_TOKEN);
452
453         // then:
454         verify(getWebserviceMock(), atLeast(1)).setAccessToken(ACCESS_TOKEN);
455         verify(getWebserviceMock(), atLeast(1)).connectSse();
456     }
457
458     @Test
459     public void whenTheHandlerIsDisposedThenTheSseConnectionIsDisconnectedAndTheLanguageProviderIsUnset()
460             throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
461         // given:
462         getHandler().initialize();
463
464         // when:
465         getHandler().dispose();
466
467         // then:
468         verify(getWebserviceMock()).disconnectSse();
469
470         CombiningLanguageProvider languageProvider = getPrivate(getHandler(), "languageProvider");
471         assertNull(getPrivate(languageProvider, "prioritizedLanguageProvider"));
472     }
473
474     @Test
475     public void testNoLanguageIsReturnedWhenTheConfigurationParameterIsNotSet() {
476         // when:
477         Optional<String> language = getHandler().getLanguage();
478
479         // then:
480         assertFalse(language.isPresent());
481     }
482
483     @Test
484     public void testNoLanguageIsReturnedWhenTheConfigurationParameterIsEmpty() {
485         // given:
486         getHandler().handleConfigurationUpdate(
487                 Map.of(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL, SERVICE_HANDLE, CONFIG_PARAM_LOCALE, ""));
488
489         // when:
490         Optional<String> language = getHandler().getLanguage();
491
492         // then:
493         assertFalse(language.isPresent());
494     }
495
496     @Test
497     public void testNoLanguageIsReturnedWhenTheConfigurationParameterIsNotAValidTwoLetterLanguageCode() {
498         // given:
499         getHandler().handleConfigurationUpdate(
500                 Map.of(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL, SERVICE_HANDLE, CONFIG_PARAM_LOCALE, "Deutsch"));
501
502         // when:
503         Optional<String> language = getHandler().getLanguage();
504
505         // then:
506         assertFalse(language.isPresent());
507     }
508
509     @Test
510     public void testAValidTwoLetterLanguageCodeIsReturnedWhenTheConfigurationParameterIsSetToTheTwoLetterLanguageCode() {
511         // given:
512         getHandler().handleConfigurationUpdate(
513                 Map.of(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL, SERVICE_HANDLE, CONFIG_PARAM_LOCALE, "DE"));
514
515         // when:
516         String language = getHandler().getLanguage().get();
517
518         // then:
519         assertEquals("DE", language);
520     }
521
522     @Test
523     public void testWhenTheThingIsRemovedThenTheWebserviceIsLoggedOut() throws Exception {
524         // given:
525         initializeBridgeWithTokens();
526
527         // when:
528         getThingRegistry().remove(getHandler().getThing().getUID());
529
530         // then:
531         waitForAssert(() -> {
532             verify(getWebserviceMock()).logout();
533         });
534     }
535
536     @Test
537     public void testWhenTheThingIsRemovedThenTheTokensAreRemovedFromTheStorage() throws Exception {
538         // given:
539         initializeBridgeWithTokens();
540
541         // when:
542         getThingRegistry().remove(getHandler().getThing().getUID());
543
544         // then:
545         waitForAssert(() -> {
546             verify(getOAuthFactoryMock()).deleteServiceAndAccessToken(SERVICE_HANDLE);
547         });
548     }
549 }