]> git.basschouten.com Git - openhab-addons.git/blob
66fa0f04e51a8bac87d7c94369186d801e2e221e
[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 testThingStatusIsSetToOfflineWithDetailConfigurationErrorAndDescriptionWhenTheMieleAccountHasNotBeenAuthorized()
200             throws Exception {
201         // given:
202         OAuthFactory oAuthFactory = mock(OAuthFactory.class);
203         Mockito.when(oAuthFactory.getOAuthClientService(SERVICE_HANDLE)).thenReturn(null);
204
205         OpenHabOAuthTokenRefresher tokenRefresher = getService(OAuthTokenRefresher.class,
206                 OpenHabOAuthTokenRefresher.class);
207         assertNotNull(tokenRefresher);
208         // Clear the setup configuration and use the failing one for this test.
209         setPrivate(Objects.requireNonNull(tokenRefresher), "oauthFactory", oAuthFactory);
210
211         // when:
212         getHandler().initialize();
213
214         // then:
215         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
216                 MieleCloudBindingConstants.I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCOUNT_NOT_AUTHORIZED);
217     }
218
219     @Test
220     public void testThingStatusIsSetToUnknownAndThingWaitsForCloudConnectionWhenTheMieleAccountBecomesAuthorizedAfterTheBridgeWasInitialized()
221             throws Exception {
222         // given:
223         OAuthFactory oAuthFactory = mock(OAuthFactory.class);
224         Mockito.when(oAuthFactory.getOAuthClientService(SERVICE_HANDLE)).thenReturn(null);
225
226         OpenHabOAuthTokenRefresher tokenRefresher = getService(OAuthTokenRefresher.class,
227                 OpenHabOAuthTokenRefresher.class);
228         assertNotNull(tokenRefresher);
229         // Clear the setup configuration and use the failing one for this test.
230         setPrivate(Objects.requireNonNull(tokenRefresher), "oauthFactory", oAuthFactory);
231
232         getHandler().initialize();
233
234         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
235                 I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCOUNT_NOT_AUTHORIZED);
236
237         setUpOAuthFactory();
238
239         // when:
240         getHandler().dispose();
241         getHandler().initialize();
242
243         // then:
244         assertThingStatusIs(ThingStatus.UNKNOWN);
245     }
246
247     @Test
248     public void whenTheSseConnectionIsEstablishedThenTheThingStatusIsSetToOnline() throws Exception {
249         // given:
250         initializeBridgeWithTokens();
251
252         // when:
253         getHandler().onConnectionAlive();
254
255         // then:
256         assertThingStatusIs(ThingStatus.ONLINE);
257     }
258
259     @Test
260     public void whenAnAuthorizationFailedErrorIsReportedThenTheAccessTokenIsRefreshedAndTheSseConnectionRestored()
261             throws Exception {
262         // given:
263         AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
264         accessTokenResponse.setAccessToken(ACCESS_TOKEN);
265         when(getOAuthClientServiceMock().refreshToken()).thenReturn(accessTokenResponse);
266
267         initializeBridgeWithTokens();
268         getHandler().onConnectionAlive();
269
270         // when:
271         getHandler().onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
272
273         // then:
274         verify(getOAuthClientServiceMock()).refreshToken();
275         verify(getWebserviceMock()).connectSse();
276         assertThingStatusIs(ThingStatus.ONLINE);
277     }
278
279     @Test
280     public void whenAnAuthorizationFailedErrorIsReportedAndTokenRefreshFailsThenSseConnectionIsTerminatedAndTheStatusSetToOfflineWithDetailConfigurationError()
281             throws Exception {
282         // given:
283         when(getOAuthClientServiceMock().refreshToken()).thenReturn(new AccessTokenResponse());
284         initializeBridgeWithTokens();
285         getHandler().onConnectionAlive();
286
287         // when:
288         getHandler().onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
289
290         // then:
291         verify(getOAuthClientServiceMock()).refreshToken();
292         verify(getWebserviceMock()).disconnectSse();
293         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
294                 I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_REFRESH_FAILED);
295     }
296
297     @Test
298     public void whenARequestExecutionFailedErrorIsReportedAndNoRetriesHaveBeenMadeThenItHasNoEffectOnTheThingStatus()
299             throws Exception {
300         // given:
301         initializeBridgeWithTokens();
302         getHandler().onConnectionAlive();
303
304         // when:
305         getHandler().onConnectionError(ConnectionError.REQUEST_EXECUTION_FAILED, 0);
306
307         // then:
308         assertThingStatusIs(ThingStatus.ONLINE);
309     }
310
311     @Test
312     public void whenARequestExecutionFailedErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
313             throws Exception {
314         // given:
315         initializeBridgeWithTokens();
316         getHandler().onConnectionAlive();
317
318         // when:
319         getHandler().onConnectionError(ConnectionError.REQUEST_EXECUTION_FAILED, 10);
320
321         // then:
322         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
323     }
324
325     @Test
326     public void whenARequestExecutionFailedErrorIsReportedAndThingIsInStatusUnknownThenTheThingStatusIsOfflineWithDetailCommunicationError()
327             throws Exception {
328         // given:
329         initializeBridgeWithTokens();
330
331         // when:
332         getHandler().onConnectionError(ConnectionError.REQUEST_EXECUTION_FAILED, 0);
333
334         // then:
335         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
336     }
337
338     @Test
339     public void whenAServiceUnavailableErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
340             throws Exception {
341         // given:
342         initializeBridgeWithTokens();
343         getHandler().onConnectionAlive();
344
345         // when:
346         getHandler().onConnectionError(ConnectionError.SERVICE_UNAVAILABLE, 10);
347
348         // then:
349         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
350     }
351
352     @Test
353     public void whenAResponseMalformedErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
354             throws Exception {
355         // given:
356         initializeBridgeWithTokens();
357
358         // when:
359         getHandler().onConnectionError(ConnectionError.RESPONSE_MALFORMED, 10);
360
361         // then:
362         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
363     }
364
365     @Test
366     public void whenATimeoutErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
367             throws Exception {
368         // given:
369         initializeBridgeWithTokens();
370
371         // when:
372         getHandler().onConnectionError(ConnectionError.TIMEOUT, 10);
373
374         // then:
375         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
376     }
377
378     @Test
379     public void whenATooManyRequestsErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
380             throws Exception {
381         // given:
382         initializeBridgeWithTokens();
383
384         // when:
385         getHandler().onConnectionError(ConnectionError.TOO_MANY_RERQUESTS, 10);
386
387         // then:
388         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
389     }
390
391     @Test
392     public void whenAServerErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
393             throws Exception {
394         // given:
395         initializeBridgeWithTokens();
396
397         // when:
398         getHandler().onConnectionError(ConnectionError.SERVER_ERROR, 10);
399
400         // then:
401         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
402                 I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
403     }
404
405     @Test
406     public void whenARequestInterruptedErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
407             throws Exception {
408         // given:
409         initializeBridgeWithTokens();
410
411         // when:
412         getHandler().onConnectionError(ConnectionError.REQUEST_INTERRUPTED, 10);
413
414         // then:
415         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
416                 I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
417     }
418
419     @Test
420     public void whenSomeOtherHttpErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
421             throws Exception {
422         // given:
423         initializeBridgeWithTokens();
424
425         // when:
426         getHandler().onConnectionError(ConnectionError.OTHER_HTTP_ERROR, 10);
427
428         // then:
429         assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
430                 I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
431     }
432
433     @Test
434     public void whenARequestIsInterruptedDuringInitializationThenTheThingStatusIsNotModified() throws Exception {
435         // given:
436         initializeBridgeWithTokens();
437
438         // when:
439         getHandler().onConnectionError(ConnectionError.REQUEST_INTERRUPTED, 0);
440
441         // then:
442         assertThingStatusIs(ThingStatus.UNKNOWN);
443     }
444
445     @Test
446     public void whenTheAccessTokenWasRefreshedThenTheWebserviceIsSetIntoAnOperationalState()
447             throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
448         // given:
449         getHandler().initialize();
450
451         // when:
452         getHandler().onNewAccessToken(ACCESS_TOKEN);
453
454         // then:
455         verify(getWebserviceMock(), atLeast(1)).setAccessToken(ACCESS_TOKEN);
456         verify(getWebserviceMock(), atLeast(1)).connectSse();
457     }
458
459     @Test
460     public void whenTheHandlerIsDisposedThenTheSseConnectionIsDisconnectedAndTheLanguageProviderIsUnset()
461             throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
462         // given:
463         getHandler().initialize();
464
465         // when:
466         getHandler().dispose();
467
468         // then:
469         verify(getWebserviceMock()).disconnectSse();
470
471         CombiningLanguageProvider languageProvider = getPrivate(getHandler(), "languageProvider");
472         assertNull(getPrivate(languageProvider, "prioritizedLanguageProvider"));
473     }
474
475     @Test
476     public void testNoLanguageIsReturnedWhenTheConfigurationParameterIsNotSet() {
477         // when:
478         Optional<String> language = getHandler().getLanguage();
479
480         // then:
481         assertFalse(language.isPresent());
482     }
483
484     @Test
485     public void testNoLanguageIsReturnedWhenTheConfigurationParameterIsEmpty() {
486         // given:
487         getHandler().handleConfigurationUpdate(Collections.singletonMap(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(Collections.singletonMap(CONFIG_PARAM_LOCALE, "Deutsch"));
500
501         // when:
502         Optional<String> language = getHandler().getLanguage();
503
504         // then:
505         assertFalse(language.isPresent());
506     }
507
508     @Test
509     public void testAValidTwoLetterLanguageCodeIsReturnedWhenTheConfigurationParameterIsSetToTheTwoLetterLanguageCode() {
510         // given:
511         getHandler().handleConfigurationUpdate(Collections.singletonMap(CONFIG_PARAM_LOCALE, "DE"));
512
513         // when:
514         String language = getHandler().getLanguage().get();
515
516         // then:
517         assertEquals("DE", language);
518     }
519
520     @Test
521     public void testWhenTheThingIsRemovedThenTheWebserviceIsLoggedOut() throws Exception {
522         // given:
523         initializeBridgeWithTokens();
524
525         // when:
526         getThingRegistry().remove(getHandler().getThing().getUID());
527
528         // then:
529         waitForAssert(() -> {
530             verify(getWebserviceMock()).logout();
531         });
532     }
533
534     @Test
535     public void testWhenTheThingIsRemovedThenTheTokensAreRemovedFromTheStorage() throws Exception {
536         // given:
537         initializeBridgeWithTokens();
538
539         // when:
540         getThingRegistry().remove(getHandler().getThing().getUID());
541
542         // then:
543         waitForAssert(() -> {
544             verify(getOAuthFactoryMock()).deleteServiceAndAccessToken(SERVICE_HANDLE);
545         });
546     }
547 }