2 * Copyright (c) 2010-2022 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 testThingStatusIsSetToOfflineWithDetailConfigurationErrorAndDescriptionWhenTheMieleAccountHasNotBeenAuthorized()
202 OAuthFactory oAuthFactory = mock(OAuthFactory.class);
203 Mockito.when(oAuthFactory.getOAuthClientService(SERVICE_HANDLE)).thenReturn(null);
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);
212 getHandler().initialize();
215 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
216 MieleCloudBindingConstants.I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCOUNT_NOT_AUTHORIZED);
220 public void testThingStatusIsSetToUnknownAndThingWaitsForCloudConnectionWhenTheMieleAccountBecomesAuthorizedAfterTheBridgeWasInitialized()
223 OAuthFactory oAuthFactory = mock(OAuthFactory.class);
224 Mockito.when(oAuthFactory.getOAuthClientService(SERVICE_HANDLE)).thenReturn(null);
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);
232 getHandler().initialize();
234 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
235 I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCOUNT_NOT_AUTHORIZED);
240 getHandler().dispose();
241 getHandler().initialize();
244 assertThingStatusIs(ThingStatus.UNKNOWN);
248 public void whenTheSseConnectionIsEstablishedThenTheThingStatusIsSetToOnline() throws Exception {
250 initializeBridgeWithTokens();
253 getHandler().onConnectionAlive();
256 assertThingStatusIs(ThingStatus.ONLINE);
260 public void whenAnAuthorizationFailedErrorIsReportedThenTheAccessTokenIsRefreshedAndTheSseConnectionRestored()
263 AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
264 accessTokenResponse.setAccessToken(ACCESS_TOKEN);
265 when(getOAuthClientServiceMock().refreshToken()).thenReturn(accessTokenResponse);
267 initializeBridgeWithTokens();
268 getHandler().onConnectionAlive();
271 getHandler().onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
274 verify(getOAuthClientServiceMock()).refreshToken();
275 verify(getWebserviceMock()).connectSse();
276 assertThingStatusIs(ThingStatus.ONLINE);
280 public void whenAnAuthorizationFailedErrorIsReportedAndTokenRefreshFailsThenSseConnectionIsTerminatedAndTheStatusSetToOfflineWithDetailConfigurationError()
283 when(getOAuthClientServiceMock().refreshToken()).thenReturn(new AccessTokenResponse());
284 initializeBridgeWithTokens();
285 getHandler().onConnectionAlive();
288 getHandler().onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
291 verify(getOAuthClientServiceMock()).refreshToken();
292 verify(getWebserviceMock()).disconnectSse();
293 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
294 I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_REFRESH_FAILED);
298 public void whenARequestExecutionFailedErrorIsReportedAndNoRetriesHaveBeenMadeThenItHasNoEffectOnTheThingStatus()
301 initializeBridgeWithTokens();
302 getHandler().onConnectionAlive();
305 getHandler().onConnectionError(ConnectionError.REQUEST_EXECUTION_FAILED, 0);
308 assertThingStatusIs(ThingStatus.ONLINE);
312 public void whenARequestExecutionFailedErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
315 initializeBridgeWithTokens();
316 getHandler().onConnectionAlive();
319 getHandler().onConnectionError(ConnectionError.REQUEST_EXECUTION_FAILED, 10);
322 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
326 public void whenARequestExecutionFailedErrorIsReportedAndThingIsInStatusUnknownThenTheThingStatusIsOfflineWithDetailCommunicationError()
329 initializeBridgeWithTokens();
332 getHandler().onConnectionError(ConnectionError.REQUEST_EXECUTION_FAILED, 0);
335 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
339 public void whenAServiceUnavailableErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
342 initializeBridgeWithTokens();
343 getHandler().onConnectionAlive();
346 getHandler().onConnectionError(ConnectionError.SERVICE_UNAVAILABLE, 10);
349 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
353 public void whenAResponseMalformedErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
356 initializeBridgeWithTokens();
359 getHandler().onConnectionError(ConnectionError.RESPONSE_MALFORMED, 10);
362 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
366 public void whenATimeoutErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
369 initializeBridgeWithTokens();
372 getHandler().onConnectionError(ConnectionError.TIMEOUT, 10);
375 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
379 public void whenATooManyRequestsErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
382 initializeBridgeWithTokens();
385 getHandler().onConnectionError(ConnectionError.TOO_MANY_RERQUESTS, 10);
388 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
392 public void whenAServerErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
395 initializeBridgeWithTokens();
398 getHandler().onConnectionError(ConnectionError.SERVER_ERROR, 10);
401 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
402 I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
406 public void whenARequestInterruptedErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
409 initializeBridgeWithTokens();
412 getHandler().onConnectionError(ConnectionError.REQUEST_INTERRUPTED, 10);
415 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
416 I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
420 public void whenSomeOtherHttpErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
423 initializeBridgeWithTokens();
426 getHandler().onConnectionError(ConnectionError.OTHER_HTTP_ERROR, 10);
429 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
430 I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
434 public void whenARequestIsInterruptedDuringInitializationThenTheThingStatusIsNotModified() throws Exception {
436 initializeBridgeWithTokens();
439 getHandler().onConnectionError(ConnectionError.REQUEST_INTERRUPTED, 0);
442 assertThingStatusIs(ThingStatus.UNKNOWN);
446 public void whenTheAccessTokenWasRefreshedThenTheWebserviceIsSetIntoAnOperationalState()
447 throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
449 getHandler().initialize();
452 getHandler().onNewAccessToken(ACCESS_TOKEN);
455 verify(getWebserviceMock(), atLeast(1)).setAccessToken(ACCESS_TOKEN);
456 verify(getWebserviceMock(), atLeast(1)).connectSse();
460 public void whenTheHandlerIsDisposedThenTheSseConnectionIsDisconnectedAndTheLanguageProviderIsUnset()
461 throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
463 getHandler().initialize();
466 getHandler().dispose();
469 verify(getWebserviceMock()).disconnectSse();
471 CombiningLanguageProvider languageProvider = getPrivate(getHandler(), "languageProvider");
472 assertNull(getPrivate(languageProvider, "prioritizedLanguageProvider"));
476 public void testNoLanguageIsReturnedWhenTheConfigurationParameterIsNotSet() {
478 Optional<String> language = getHandler().getLanguage();
481 assertFalse(language.isPresent());
485 public void testNoLanguageIsReturnedWhenTheConfigurationParameterIsEmpty() {
487 getHandler().handleConfigurationUpdate(Collections.singletonMap(CONFIG_PARAM_LOCALE, ""));
490 Optional<String> language = getHandler().getLanguage();
493 assertFalse(language.isPresent());
497 public void testNoLanguageIsReturnedWhenTheConfigurationParameterIsNotAValidTwoLetterLanguageCode() {
499 getHandler().handleConfigurationUpdate(Collections.singletonMap(CONFIG_PARAM_LOCALE, "Deutsch"));
502 Optional<String> language = getHandler().getLanguage();
505 assertFalse(language.isPresent());
509 public void testAValidTwoLetterLanguageCodeIsReturnedWhenTheConfigurationParameterIsSetToTheTwoLetterLanguageCode() {
511 getHandler().handleConfigurationUpdate(Collections.singletonMap(CONFIG_PARAM_LOCALE, "DE"));
514 String language = getHandler().getLanguage().get();
517 assertEquals("DE", language);
521 public void testWhenTheThingIsRemovedThenTheWebserviceIsLoggedOut() throws Exception {
523 initializeBridgeWithTokens();
526 getThingRegistry().remove(getHandler().getThing().getUID());
529 waitForAssert(() -> {
530 verify(getWebserviceMock()).logout();
535 public void testWhenTheThingIsRemovedThenTheTokensAreRemovedFromTheStorage() throws Exception {
537 initializeBridgeWithTokens();
540 getThingRegistry().remove(getHandler().getThing().getUID());
543 waitForAssert(() -> {
544 verify(getOAuthFactoryMock()).deleteServiceAndAccessToken(SERVICE_HANDLE);