2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.mielecloud.internal.handler;
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.*;
21 import java.util.Collections;
22 import java.util.Objects;
23 import java.util.Optional;
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;
51 * @author Björn Lange - Initial contribution
54 public class MieleBridgeHandlerTest extends OpenHabOsgiTest {
55 private static final String SERVICE_HANDLE = MieleCloudBindingIntegrationTestConstants.EMAIL;
56 private static final String CONFIG_PARAM_LOCALE = "locale";
59 private MieleWebservice webserviceMock;
61 private String webserviceAccessToken;
63 private OAuthFactory oauthFactoryMock;
65 private OAuthClientService oauthClientServiceMock;
68 private Bridge bridge;
70 private MieleBridgeHandler handler;
72 private MieleWebservice getWebserviceMock() {
73 assertNotNull(webserviceMock);
74 return Objects.requireNonNull(webserviceMock);
77 private OAuthFactory getOAuthFactoryMock() {
78 assertNotNull(oauthFactoryMock);
79 return Objects.requireNonNull(oauthFactoryMock);
82 private OAuthClientService getOAuthClientServiceMock() {
83 OAuthClientService oauthClientServiceMock = this.oauthClientServiceMock;
84 assertNotNull(oauthClientServiceMock);
85 return Objects.requireNonNull(oauthClientServiceMock);
88 private Bridge getBridge() {
89 assertNotNull(bridge);
90 return Objects.requireNonNull(bridge);
93 private MieleBridgeHandler getHandler() {
94 assertNotNull(handler);
95 return Objects.requireNonNull(handler);
99 public void setUp() throws Exception {
101 setUpBridgeThingAndHandler();
105 private void setUpWebservice() throws NoSuchFieldException, IllegalAccessException {
106 webserviceMock = mock(MieleWebservice.class);
107 doAnswer(invocation -> {
108 if (invocation != null) {
109 webserviceAccessToken = invocation.getArgument(0);
112 }).when(getWebserviceMock()).setAccessToken(anyString());
113 when(getWebserviceMock().hasAccessToken()).then(invocation -> webserviceAccessToken != null);
115 MieleWebserviceFactory webserviceFactory = mock(MieleWebserviceFactory.class);
116 when(webserviceFactory.create(any())).thenReturn(getWebserviceMock());
118 MieleHandlerFactory handlerFactory = getService(ThingHandlerFactory.class, MieleHandlerFactory.class);
119 assertNotNull(handlerFactory);
120 setPrivate(Objects.requireNonNull(handlerFactory), "webserviceFactory", webserviceFactory);
123 private void setUpBridgeThingAndHandler() {
124 when(getWebserviceMock().hasAccessToken()).thenReturn(false);
126 bridge = BridgeBuilder
127 .create(MieleCloudBindingConstants.THING_TYPE_BRIDGE,
128 MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID)
130 new Configuration(Collections.singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL,
131 MieleCloudBindingIntegrationTestConstants.EMAIL)))
132 .withLabel(MIELE_CLOUD_ACCOUNT_LABEL).build();
133 assertNotNull(bridge);
135 getThingRegistry().add(getBridge());
137 waitForAssert(() -> {
138 assertNotNull(getBridge().getHandler());
139 assertTrue(getBridge().getHandler() instanceof MieleBridgeHandler, "Handler type is wrong");
141 handler = (MieleBridgeHandler) getBridge().getHandler();
144 private void setUpOAuthFactory() throws Exception {
145 AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
146 accessTokenResponse.setAccessToken(ACCESS_TOKEN);
148 oauthClientServiceMock = mock(OAuthClientService.class);
149 when(oauthClientServiceMock.getAccessTokenResponse()).thenReturn(accessTokenResponse);
151 OAuthFactory oAuthFactory = mock(OAuthFactory.class);
152 Mockito.when(oAuthFactory.getOAuthClientService(SERVICE_HANDLE)).thenReturn(getOAuthClientServiceMock());
153 oauthFactoryMock = oAuthFactory;
155 OpenHabOAuthTokenRefresher tokenRefresher = getService(OAuthTokenRefresher.class,
156 OpenHabOAuthTokenRefresher.class);
157 assertNotNull(tokenRefresher);
158 setPrivate(Objects.requireNonNull(tokenRefresher), "oauthFactory", oAuthFactory);
161 private void initializeBridgeWithTokens() {
162 getHandler().initialize();
163 assertThingStatusIs(ThingStatus.UNKNOWN);
166 private void assertThingStatusIs(ThingStatus expectedStatus) {
167 assertThingStatusIs(expectedStatus, ThingStatusDetail.NONE);
170 private void assertThingStatusIs(ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail) {
171 assertThingStatusIs(expectedStatus, expectedStatusDetail, null);
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());
181 assertEquals(expectedDescription, getBridge().getStatusInfo().getDescription());
186 public void testThingStatusIsSetToOfflineWithDetailConfigurationPendingAndDescriptionWhenTokensAreNotPassedViaInitialConfiguration()
188 when(getOAuthClientServiceMock().getAccessTokenResponse()).thenReturn(null);
191 getHandler().initialize();
194 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
195 MieleCloudBindingConstants.I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_NOT_CONFIGURED);
199 public void testThingStatusIsSetToOfflineWithDetailConfigurationErrorAndDescriptionWhenTheEmailAddressIsInvalid()
202 getBridge().getConfiguration().setProperties(
203 Collections.singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL, "not!a!mail$address"));
206 getHandler().initialize();
209 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
210 MieleCloudBindingConstants.I18NKeys.BRIDGE_STATUS_DESCRIPTION_INVALID_EMAIL);
214 public void testThingStatusIsSetToOfflineWithDetailConfigurationErrorAndDescriptionWhenTheMieleAccountHasNotBeenAuthorized()
217 OAuthFactory oAuthFactory = mock(OAuthFactory.class);
218 Mockito.when(oAuthFactory.getOAuthClientService(SERVICE_HANDLE)).thenReturn(null);
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);
227 getHandler().initialize();
230 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
231 MieleCloudBindingConstants.I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCOUNT_NOT_AUTHORIZED);
235 public void testThingStatusIsSetToUnknownAndThingWaitsForCloudConnectionWhenTheMieleAccountBecomesAuthorizedAfterTheBridgeWasInitialized()
238 OAuthFactory oAuthFactory = mock(OAuthFactory.class);
239 Mockito.when(oAuthFactory.getOAuthClientService(SERVICE_HANDLE)).thenReturn(null);
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);
247 getHandler().initialize();
249 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
250 I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCOUNT_NOT_AUTHORIZED);
255 getHandler().dispose();
256 getHandler().initialize();
259 assertThingStatusIs(ThingStatus.UNKNOWN);
263 public void whenTheSseConnectionIsEstablishedThenTheThingStatusIsSetToOnline() throws Exception {
265 initializeBridgeWithTokens();
268 getHandler().onConnectionAlive();
271 assertThingStatusIs(ThingStatus.ONLINE);
275 public void whenAnAuthorizationFailedErrorIsReportedThenTheAccessTokenIsRefreshedAndTheSseConnectionRestored()
278 AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
279 accessTokenResponse.setAccessToken(ACCESS_TOKEN);
280 when(getOAuthClientServiceMock().refreshToken()).thenReturn(accessTokenResponse);
282 initializeBridgeWithTokens();
283 getHandler().onConnectionAlive();
286 getHandler().onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
289 verify(getOAuthClientServiceMock()).refreshToken();
290 verify(getWebserviceMock()).connectSse();
291 assertThingStatusIs(ThingStatus.ONLINE);
295 public void whenAnAuthorizationFailedErrorIsReportedAndTokenRefreshFailsThenSseConnectionIsTerminatedAndTheStatusSetToOfflineWithDetailConfigurationError()
298 when(getOAuthClientServiceMock().refreshToken()).thenReturn(new AccessTokenResponse());
299 initializeBridgeWithTokens();
300 getHandler().onConnectionAlive();
303 getHandler().onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
306 verify(getOAuthClientServiceMock()).refreshToken();
307 verify(getWebserviceMock()).disconnectSse();
308 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
309 I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_REFRESH_FAILED);
313 public void whenARequestExecutionFailedErrorIsReportedAndNoRetriesHaveBeenMadeThenItHasNoEffectOnTheThingStatus()
316 initializeBridgeWithTokens();
317 getHandler().onConnectionAlive();
320 getHandler().onConnectionError(ConnectionError.REQUEST_EXECUTION_FAILED, 0);
323 assertThingStatusIs(ThingStatus.ONLINE);
327 public void whenARequestExecutionFailedErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
330 initializeBridgeWithTokens();
331 getHandler().onConnectionAlive();
334 getHandler().onConnectionError(ConnectionError.REQUEST_EXECUTION_FAILED, 10);
337 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
341 public void whenARequestExecutionFailedErrorIsReportedAndThingIsInStatusUnknownThenTheThingStatusIsOfflineWithDetailCommunicationError()
344 initializeBridgeWithTokens();
347 getHandler().onConnectionError(ConnectionError.REQUEST_EXECUTION_FAILED, 0);
350 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
354 public void whenAServiceUnavailableErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
357 initializeBridgeWithTokens();
358 getHandler().onConnectionAlive();
361 getHandler().onConnectionError(ConnectionError.SERVICE_UNAVAILABLE, 10);
364 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
368 public void whenAResponseMalformedErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
371 initializeBridgeWithTokens();
374 getHandler().onConnectionError(ConnectionError.RESPONSE_MALFORMED, 10);
377 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
381 public void whenATimeoutErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
384 initializeBridgeWithTokens();
387 getHandler().onConnectionError(ConnectionError.TIMEOUT, 10);
390 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
394 public void whenATooManyRequestsErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
397 initializeBridgeWithTokens();
400 getHandler().onConnectionError(ConnectionError.TOO_MANY_RERQUESTS, 10);
403 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
407 public void whenAServerErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
410 initializeBridgeWithTokens();
413 getHandler().onConnectionError(ConnectionError.SERVER_ERROR, 10);
416 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
417 I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
421 public void whenARequestInterruptedErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
424 initializeBridgeWithTokens();
427 getHandler().onConnectionError(ConnectionError.REQUEST_INTERRUPTED, 10);
430 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
431 I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
435 public void whenSomeOtherHttpErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
438 initializeBridgeWithTokens();
441 getHandler().onConnectionError(ConnectionError.OTHER_HTTP_ERROR, 10);
444 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
445 I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
449 public void whenARequestIsInterruptedDuringInitializationThenTheThingStatusIsNotModified() throws Exception {
451 initializeBridgeWithTokens();
454 getHandler().onConnectionError(ConnectionError.REQUEST_INTERRUPTED, 0);
457 assertThingStatusIs(ThingStatus.UNKNOWN);
461 public void whenTheAccessTokenWasRefreshedThenTheWebserviceIsSetIntoAnOperationalState()
462 throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
464 getHandler().initialize();
467 getHandler().onNewAccessToken(ACCESS_TOKEN);
470 verify(getWebserviceMock(), atLeast(1)).setAccessToken(ACCESS_TOKEN);
471 verify(getWebserviceMock(), atLeast(1)).connectSse();
475 public void whenTheHandlerIsDisposedThenTheSseConnectionIsDisconnectedAndTheLanguageProviderIsUnset()
476 throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
478 getHandler().initialize();
481 getHandler().dispose();
484 verify(getWebserviceMock()).disconnectSse();
486 CombiningLanguageProvider languageProvider = getPrivate(getHandler(), "languageProvider");
487 assertNull(getPrivate(languageProvider, "prioritizedLanguageProvider"));
491 public void testNoLanguageIsReturnedWhenTheConfigurationParameterIsNotSet() {
493 Optional<String> language = getHandler().getLanguage();
496 assertFalse(language.isPresent());
500 public void testNoLanguageIsReturnedWhenTheConfigurationParameterIsEmpty() {
502 getHandler().handleConfigurationUpdate(Collections.singletonMap(CONFIG_PARAM_LOCALE, ""));
505 Optional<String> language = getHandler().getLanguage();
508 assertFalse(language.isPresent());
512 public void testNoLanguageIsReturnedWhenTheConfigurationParameterIsNotAValidTwoLetterLanguageCode() {
514 getHandler().handleConfigurationUpdate(Collections.singletonMap(CONFIG_PARAM_LOCALE, "Deutsch"));
517 Optional<String> language = getHandler().getLanguage();
520 assertFalse(language.isPresent());
524 public void testAValidTwoLetterLanguageCodeIsReturnedWhenTheConfigurationParameterIsSetToTheTwoLetterLanguageCode() {
526 getHandler().handleConfigurationUpdate(Collections.singletonMap(CONFIG_PARAM_LOCALE, "DE"));
529 String language = getHandler().getLanguage().get();
532 assertEquals("DE", language);
536 public void testWhenTheThingIsRemovedThenTheWebserviceIsLoggedOut() throws Exception {
538 initializeBridgeWithTokens();
541 getThingRegistry().remove(getHandler().getThing().getUID());
544 waitForAssert(() -> {
545 verify(getWebserviceMock()).logout();
550 public void testWhenTheThingIsRemovedThenTheTokensAreRemovedFromTheStorage() throws Exception {
552 initializeBridgeWithTokens();
555 getThingRegistry().remove(getHandler().getThing().getUID());
558 waitForAssert(() -> {
559 verify(getOAuthFactoryMock()).deleteServiceAndAccessToken(SERVICE_HANDLE);