2 * Copyright (c) 2010-2023 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.*;
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)
129 .withConfiguration(new Configuration(Map.of(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL,
130 MieleCloudBindingIntegrationTestConstants.EMAIL)))
131 .withLabel(MIELE_CLOUD_ACCOUNT_LABEL).build();
132 assertNotNull(bridge);
134 getThingRegistry().add(getBridge());
136 waitForAssert(() -> {
137 assertNotNull(getBridge().getHandler());
138 assertTrue(getBridge().getHandler() instanceof MieleBridgeHandler, "Handler type is wrong");
140 handler = (MieleBridgeHandler) getBridge().getHandler();
143 private void setUpOAuthFactory() throws Exception {
144 AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
145 accessTokenResponse.setAccessToken(ACCESS_TOKEN);
147 oauthClientServiceMock = mock(OAuthClientService.class);
148 when(oauthClientServiceMock.getAccessTokenResponse()).thenReturn(accessTokenResponse);
150 OAuthFactory oAuthFactory = mock(OAuthFactory.class);
151 Mockito.when(oAuthFactory.getOAuthClientService(SERVICE_HANDLE)).thenReturn(getOAuthClientServiceMock());
152 oauthFactoryMock = oAuthFactory;
154 OpenHabOAuthTokenRefresher tokenRefresher = getService(OAuthTokenRefresher.class,
155 OpenHabOAuthTokenRefresher.class);
156 assertNotNull(tokenRefresher);
157 setPrivate(Objects.requireNonNull(tokenRefresher), "oauthFactory", oAuthFactory);
160 private void initializeBridgeWithTokens() {
161 getHandler().initialize();
162 assertThingStatusIs(ThingStatus.UNKNOWN);
165 private void assertThingStatusIs(ThingStatus expectedStatus) {
166 assertThingStatusIs(expectedStatus, ThingStatusDetail.NONE);
169 private void assertThingStatusIs(ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail) {
170 assertThingStatusIs(expectedStatus, expectedStatusDetail, null);
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());
180 assertEquals(expectedDescription, getBridge().getStatusInfo().getDescription());
185 public void testThingStatusIsSetToOfflineWithDetailConfigurationPendingAndDescriptionWhenTokensAreNotPassedViaInitialConfiguration()
187 when(getOAuthClientServiceMock().getAccessTokenResponse()).thenReturn(null);
190 getHandler().initialize();
193 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
194 MieleCloudBindingConstants.I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_NOT_CONFIGURED);
198 public void testThingStatusIsSetToOfflineWithDetailConfigurationErrorAndDescriptionWhenTheMieleAccountHasNotBeenAuthorized()
201 OAuthFactory oAuthFactory = mock(OAuthFactory.class);
202 Mockito.when(oAuthFactory.getOAuthClientService(SERVICE_HANDLE)).thenReturn(null);
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);
211 getHandler().initialize();
214 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
215 MieleCloudBindingConstants.I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCOUNT_NOT_AUTHORIZED);
219 public void testThingStatusIsSetToUnknownAndThingWaitsForCloudConnectionWhenTheMieleAccountBecomesAuthorizedAfterTheBridgeWasInitialized()
222 OAuthFactory oAuthFactory = mock(OAuthFactory.class);
223 Mockito.when(oAuthFactory.getOAuthClientService(SERVICE_HANDLE)).thenReturn(null);
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);
231 getHandler().initialize();
233 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
234 I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCOUNT_NOT_AUTHORIZED);
239 getHandler().dispose();
240 getHandler().initialize();
243 assertThingStatusIs(ThingStatus.UNKNOWN);
247 public void whenTheSseConnectionIsEstablishedThenTheThingStatusIsSetToOnline() throws Exception {
249 initializeBridgeWithTokens();
252 getHandler().onConnectionAlive();
255 assertThingStatusIs(ThingStatus.ONLINE);
259 public void whenAnAuthorizationFailedErrorIsReportedThenTheAccessTokenIsRefreshedAndTheSseConnectionRestored()
262 AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
263 accessTokenResponse.setAccessToken(ACCESS_TOKEN);
264 when(getOAuthClientServiceMock().refreshToken()).thenReturn(accessTokenResponse);
266 initializeBridgeWithTokens();
267 getHandler().onConnectionAlive();
270 getHandler().onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
273 verify(getOAuthClientServiceMock()).refreshToken();
274 verify(getWebserviceMock()).connectSse();
275 assertThingStatusIs(ThingStatus.ONLINE);
279 public void whenAnAuthorizationFailedErrorIsReportedAndTokenRefreshFailsThenSseConnectionIsTerminatedAndTheStatusSetToOfflineWithDetailConfigurationError()
282 when(getOAuthClientServiceMock().refreshToken()).thenReturn(new AccessTokenResponse());
283 initializeBridgeWithTokens();
284 getHandler().onConnectionAlive();
287 getHandler().onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
290 verify(getOAuthClientServiceMock()).refreshToken();
291 verify(getWebserviceMock()).disconnectSse();
292 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
293 I18NKeys.BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_REFRESH_FAILED);
297 public void whenARequestExecutionFailedErrorIsReportedAndNoRetriesHaveBeenMadeThenItHasNoEffectOnTheThingStatus()
300 initializeBridgeWithTokens();
301 getHandler().onConnectionAlive();
304 getHandler().onConnectionError(ConnectionError.REQUEST_EXECUTION_FAILED, 0);
307 assertThingStatusIs(ThingStatus.ONLINE);
311 public void whenARequestExecutionFailedErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
314 initializeBridgeWithTokens();
315 getHandler().onConnectionAlive();
318 getHandler().onConnectionError(ConnectionError.REQUEST_EXECUTION_FAILED, 10);
321 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
325 public void whenARequestExecutionFailedErrorIsReportedAndThingIsInStatusUnknownThenTheThingStatusIsOfflineWithDetailCommunicationError()
328 initializeBridgeWithTokens();
331 getHandler().onConnectionError(ConnectionError.REQUEST_EXECUTION_FAILED, 0);
334 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
338 public void whenAServiceUnavailableErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
341 initializeBridgeWithTokens();
342 getHandler().onConnectionAlive();
345 getHandler().onConnectionError(ConnectionError.SERVICE_UNAVAILABLE, 10);
348 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
352 public void whenAResponseMalformedErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
355 initializeBridgeWithTokens();
358 getHandler().onConnectionError(ConnectionError.RESPONSE_MALFORMED, 10);
361 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
365 public void whenATimeoutErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
368 initializeBridgeWithTokens();
371 getHandler().onConnectionError(ConnectionError.TIMEOUT, 10);
374 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
378 public void whenATooManyRequestsErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
381 initializeBridgeWithTokens();
384 getHandler().onConnectionError(ConnectionError.TOO_MANY_RERQUESTS, 10);
387 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
391 public void whenAServerErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
394 initializeBridgeWithTokens();
397 getHandler().onConnectionError(ConnectionError.SERVER_ERROR, 10);
400 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
401 I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
405 public void whenARequestInterruptedErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
408 initializeBridgeWithTokens();
411 getHandler().onConnectionError(ConnectionError.REQUEST_INTERRUPTED, 10);
414 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
415 I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
419 public void whenSomeOtherHttpErrorIsReportedWithSufficientRetriesThenTheThingStatusIsOfflineWithDetailCommunicationError()
422 initializeBridgeWithTokens();
425 getHandler().onConnectionError(ConnectionError.OTHER_HTTP_ERROR, 10);
428 assertThingStatusIs(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
429 I18NKeys.BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR);
433 public void whenARequestIsInterruptedDuringInitializationThenTheThingStatusIsNotModified() throws Exception {
435 initializeBridgeWithTokens();
438 getHandler().onConnectionError(ConnectionError.REQUEST_INTERRUPTED, 0);
441 assertThingStatusIs(ThingStatus.UNKNOWN);
445 public void whenTheAccessTokenWasRefreshedThenTheWebserviceIsSetIntoAnOperationalState()
446 throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
448 getHandler().initialize();
451 getHandler().onNewAccessToken(ACCESS_TOKEN);
454 verify(getWebserviceMock(), atLeast(1)).setAccessToken(ACCESS_TOKEN);
455 verify(getWebserviceMock(), atLeast(1)).connectSse();
459 public void whenTheHandlerIsDisposedThenTheSseConnectionIsDisconnectedAndTheLanguageProviderIsUnset()
460 throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
462 getHandler().initialize();
465 getHandler().dispose();
468 verify(getWebserviceMock()).disconnectSse();
470 CombiningLanguageProvider languageProvider = getPrivate(getHandler(), "languageProvider");
471 assertNull(getPrivate(languageProvider, "prioritizedLanguageProvider"));
475 public void testNoLanguageIsReturnedWhenTheConfigurationParameterIsNotSet() {
477 Optional<String> language = getHandler().getLanguage();
480 assertFalse(language.isPresent());
484 public void testNoLanguageIsReturnedWhenTheConfigurationParameterIsEmpty() {
486 getHandler().handleConfigurationUpdate(
487 Map.of(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL, SERVICE_HANDLE, CONFIG_PARAM_LOCALE, ""));
490 Optional<String> language = getHandler().getLanguage();
493 assertFalse(language.isPresent());
497 public void testNoLanguageIsReturnedWhenTheConfigurationParameterIsNotAValidTwoLetterLanguageCode() {
499 getHandler().handleConfigurationUpdate(
500 Map.of(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL, SERVICE_HANDLE, CONFIG_PARAM_LOCALE, "Deutsch"));
503 Optional<String> language = getHandler().getLanguage();
506 assertFalse(language.isPresent());
510 public void testAValidTwoLetterLanguageCodeIsReturnedWhenTheConfigurationParameterIsSetToTheTwoLetterLanguageCode() {
512 getHandler().handleConfigurationUpdate(
513 Map.of(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL, SERVICE_HANDLE, CONFIG_PARAM_LOCALE, "DE"));
516 String language = getHandler().getLanguage().get();
519 assertEquals("DE", language);
523 public void testWhenTheThingIsRemovedThenTheWebserviceIsLoggedOut() throws Exception {
525 initializeBridgeWithTokens();
528 getThingRegistry().remove(getHandler().getThing().getUID());
531 waitForAssert(() -> {
532 verify(getWebserviceMock()).logout();
537 public void testWhenTheThingIsRemovedThenTheTokensAreRemovedFromTheStorage() throws Exception {
539 initializeBridgeWithTokens();
542 getThingRegistry().remove(getHandler().getThing().getUID());
545 waitForAssert(() -> {
546 verify(getOAuthFactoryMock()).deleteServiceAndAccessToken(SERVICE_HANDLE);